commit f173f255354e2d7dd94750779a95de8e0ab40b34 Author: smallchill Date: Mon Dec 24 11:58:45 2018 +0800 :tada: Initial commit. diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..8cfd370 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,21 @@ +# http://editorconfig.org +root = true + +# 空格替代Tab缩进在各种编辑工具下效果一致 +[*] +indent_style = space +indent_size = 4 +charset = utf-8 +end_of_line = lf +trim_trailing_whitespace = true +insert_final_newline = true + +[*.java] +indent_style = tab + +[*.{json,yml}] +indent_size = 2 + +[*.md] +insert_final_newline = false +trim_trailing_whitespace = false diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..987273c --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +# maven # +target + +logs + +# windows # +Thumbs.db + +# Mac # +.DS_Store + +# eclipse # +.settings +.project +.classpath +.log +*.class + +# idea # +.idea +*.iml + +# Package Files # +*.jar +*.war +*.ear +/target diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..bed53b8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,64 @@ +GNU LESSER GENERAL PUBLIC LICENSE + +Version 3, 29 June 2007 + +Copyright © 2007 Free Software Foundation, Inc. + +Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. + +This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below. + +0. Additional Definitions. + +As used herein, “this License” refers to version 3 of the GNU Lesser General Public License, and the “GNU GPL” refers to version 3 of the GNU General Public License. + +“The Library” refers to a covered work governed by this License, other than an Application or a Combined Work as defined below. + +An “Application” is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library. + +A “Combined Work” is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the “Linked Version”. + +The “Minimal Corresponding Source” for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version. + +The “Corresponding Application Code” for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work. + +1. Exception to Section 3 of the GNU GPL. + +You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL. + +2. Conveying Modified Versions. + +If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version: + +a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or +b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy. +3. Object Code Incorporating Material from Library Header Files. + +The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following: + +a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License. +b) Accompany the object code with a copy of the GNU GPL and this license document. +4. Combined Works. + +You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following: + +a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License. +b) Accompany the Combined Work with a copy of the GNU GPL and this license document. +c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document. +d) Do one of the following: +0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source. +1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version. +e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.) +5. Combined Libraries. + +You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following: + +a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License. +b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. +6. Revised Versions of the GNU Lesser General Public License. + +The Free Software Foundation may publish revised and/or new versions of the GNU Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License “or any later version” applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation. + +If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library. diff --git a/README.md b/README.md new file mode 100644 index 0000000..4c77853 --- /dev/null +++ b/README.md @@ -0,0 +1,59 @@ +## 简介 +blade-tool 是如梦技术团队作品, 是一个基于 Spring Boot 2 & Spring Cloud Finchley ,封装组合大量组件,用于快速构建中大型API、RESTful API项目的核心包。 + +注意事项 +* 注册中心为 Consul +* 基于 SpringBoot2.x 版本 以及 SpringCloud Finchley 版本 + +技术选型&文档 +* Spring Boot([查看Spring Boot学习&使用指南](http://www.jianshu.com/p/1a9fd8936bd8)) +* Spring Cloud([查看Spring Cloud学习&使用指南](https://springcloud.cc/)) +* Mybatis-Plus([查看官方文档](https://baomidou.gitee.io/mybatis-plus-doc/#/quick-start)) +* JsonWebToken([查看官方文档](https://jwt.io/)) + +## 鸣谢 +* 如梦技术([DreamLu](https://www.dreamlu.net/)) +* pigx([Pig Microservice](https://www.pig4cloud.com/zh-cn/)) +* avue([avue](https://avue.top/)) +* gitee.ltd([gitee.ltd](https://gitee.ltd/)) +* 鲸宵(鲸宵) + +## 开源协议 +LGPL([GNU Lesser General Public License](http://www.gnu.org/licenses/lgpl.html)) + +LGPL是GPL的一个为主要为类库使用设计的开源协议。和GPL要求任何使用/修改/衍生之GPL类库的的软件必须采用GPL协议不同。LGPL允许商业软件通过类库引用(link)方式使用LGPL类库而不需要开源商业软件的代码。这使得采用LGPL协议的开源代码可以被商业软件作为类库引用并发布和销售。 + +但是如果修改LGPL协议的代码或者衍生,则所有修改的代码,涉及修改部分的额外代码和衍生的代码都必须采用LGPL协议。因此LGPL协议的开源代码很适合作为第三方类库被商业软件引用,但不适合希望以LGPL协议代码为基础,通过修改和衍生的方式做二次开发的商业软件采用。 + +## 用户权益 +* 允许以引入不改源码的形式用于学习、毕设、公司项目、私活等 +* 特殊情况修改代码,但仍然想闭源需经过作者同意 + +## 禁止事项 +* 直接将本项目挂淘宝等商业平台出售。 + +* 非界面代码60%以上相识度的二次开源,二次开源需先联系本人。 + +注意:若禁止条款被发现有权追讨19999的授权费。 + +## 业务系统(开源协议为Apache License)界面一览(即将开源敬请期待) +![业务系统](https://raw.githubusercontent.com/chillzhuang/blade-tool/master/pic/springblade-k8s.png "业务系统") +![业务系统](https://raw.githubusercontent.com/chillzhuang/blade-tool/master/pic/springblade-traefik.png "业务系统") +![业务系统](https://raw.githubusercontent.com/chillzhuang/blade-tool/master/pic/springblade-traefik-health.png "业务系统") +![业务系统](https://raw.githubusercontent.com/chillzhuang/blade-tool/master/pic/springblade-harbor.png "业务系统") +![业务系统](https://raw.githubusercontent.com/chillzhuang/blade-tool/master/pic/springblade-consul.png "业务系统") +![业务系统](https://raw.githubusercontent.com/chillzhuang/blade-tool/master/pic/springblade-consul-nodes1.png "业务系统") +![业务系统](https://raw.githubusercontent.com/chillzhuang/blade-tool/master/pic/springblade-consul-nodes2.png "业务系统") +![业务系统](https://raw.githubusercontent.com/chillzhuang/blade-tool/master/pic/springblade-admin1.png "业务系统") +![业务系统](https://raw.githubusercontent.com/chillzhuang/blade-tool/master/pic/springblade-admin2.png "业务系统") +![业务系统](https://raw.githubusercontent.com/chillzhuang/blade-tool/master/pic/springblade-swagger1.png "业务系统") +![业务系统](https://raw.githubusercontent.com/chillzhuang/blade-tool/master/pic/springblade-swagger2.png "业务系统") +![业务系统](https://raw.githubusercontent.com/chillzhuang/blade-tool/master/pic/sword-menu.png "业务系统") +![业务系统](https://raw.githubusercontent.com/chillzhuang/blade-tool/master/pic/sword-menu-edit.png "业务系统") +![业务系统](https://raw.githubusercontent.com/chillzhuang/blade-tool/master/pic/sword-menu-icon.png "业务系统") +![业务系统](https://raw.githubusercontent.com/chillzhuang/blade-tool/master/pic/sword-role.png "业务系统") +![业务系统](https://raw.githubusercontent.com/chillzhuang/blade-tool/master/pic/sword-user.png "业务系统") +![业务系统](https://raw.githubusercontent.com/chillzhuang/blade-tool/master/pic/sword-dict.png "业务系统") +![业务系统](https://raw.githubusercontent.com/chillzhuang/blade-tool/master/pic/sword-locale-cn.png "业务系统") +![业务系统](https://raw.githubusercontent.com/chillzhuang/blade-tool/master/pic/sword-locale-us.png "业务系统") +![业务系统](https://raw.githubusercontent.com/chillzhuang/blade-tool/master/pic/sword-log.png "业务系统") \ No newline at end of file diff --git a/blade-core-boot/pom.xml b/blade-core-boot/pom.xml new file mode 100644 index 0000000..67b5149 --- /dev/null +++ b/blade-core-boot/pom.xml @@ -0,0 +1,122 @@ + + + + org.springblade + blade-tool + 1.0.0-RC1 + + + 4.0.0 + + blade-core-boot + ${project.artifactId} + ${blade.tool.version} + jar + + + + + org.springframework.boot + spring-boot-starter-jdbc + + + tomcat-jdbc + org.apache.tomcat + + + + + org.springframework.boot + spring-boot-starter-cache + + + org.springframework.cloud + spring-cloud-starter-config + + + + org.springblade + blade-core-launch + ${blade.tool.version} + + + org.springblade + blade-core-tool + ${blade.tool.version} + + + org.springblade + blade-core-secure + ${blade.tool.version} + + + org.springblade + blade-core-log + ${blade.tool.version} + + + org.springblade + blade-core-swagger + ${blade.tool.version} + + + + io.springfox + springfox-swagger2 + ${swagger.version} + + + io.swagger + swagger-models + + + + + io.swagger + swagger-models + ${swagger.models.version} + + + com.github.xiaoymin + swagger-bootstrap-ui + ${swagger.bootstrapui.version} + + + + org.springblade + blade-core-mybatis + ${blade.tool.version} + + + com.baomidou + mybatis-plus-boot-starter + ${mybatis.plus.version} + + + org.mybatis + mybatis-typehandlers-jsr310 + 1.0.2 + + + + net.sf.ehcache + ehcache + 2.10.5 + + + + mysql + mysql-connector-java + runtime + + + + + + diff --git a/blade-core-boot/src/main/java/org/springblade/core/boot/config/BladeBootAutoConfiguration.java b/blade-core-boot/src/main/java/org/springblade/core/boot/config/BladeBootAutoConfiguration.java new file mode 100644 index 0000000..9f83284 --- /dev/null +++ b/blade-core-boot/src/main/java/org/springblade/core/boot/config/BladeBootAutoConfiguration.java @@ -0,0 +1,75 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.boot.config; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springblade.core.launch.props.BladeProperties; +import org.springblade.core.tool.constant.SystemConstant; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.EnableAspectJAutoProxy; + +@Slf4j +@Configuration +@EnableConfigurationProperties({ + BladeProperties.class +}) +@EnableAspectJAutoProxy(proxyTargetClass = true, exposeProxy = true) +@AllArgsConstructor +public class BladeBootAutoConfiguration { + + private BladeProperties bladeProperties; + + /** + * 全局变量定义 + */ + @Bean + public SystemConstant fileConst() { + SystemConstant me = SystemConstant.me(); + + //设定开发模式 + me.setDevMode((bladeProperties.getEnv().equals("dev") ? true : false)); + + //设定文件上传远程地址 + me.setDomain(bladeProperties.get("upload-domain", "http://localhost:8888")); + + //设定文件上传是否为远程模式 + me.setRemoteMode(bladeProperties.getBoolean("remote-mode", true)); + + //远程上传地址 + me.setRemotePath(bladeProperties.get("remote-path", System.getProperty("user.dir") + "/work/blade")); + + //设定文件上传头文件夹 + me.setUploadPath(bladeProperties.get("upload-path", "/upload")); + + //设定文件下载头文件夹 + me.setDownloadPath(bladeProperties.get("download-path", "/download")); + + //设定上传图片是否压缩 + me.setCompress(bladeProperties.getBoolean("compress", false)); + + //设定上传图片压缩比例 + me.setCompressScale(bladeProperties.getDouble("compress-scale", 2.00)); + + //设定上传图片缩放选择:true放大;false缩小 + me.setCompressFlag(bladeProperties.getBoolean("compress-flag", false)); + + return me; + } + +} diff --git a/blade-core-boot/src/main/java/org/springblade/core/boot/config/BladeWebMvcConfiguration.java b/blade-core-boot/src/main/java/org/springblade/core/boot/config/BladeWebMvcConfiguration.java new file mode 100644 index 0000000..a903150 --- /dev/null +++ b/blade-core-boot/src/main/java/org/springblade/core/boot/config/BladeWebMvcConfiguration.java @@ -0,0 +1,59 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.boot.config; + +import feign.RequestInterceptor; +import lombok.extern.slf4j.Slf4j; +import org.springblade.core.boot.feign.BladeFeignRequestHeaderInterceptor; +import org.springblade.core.boot.feign.FeignHystrixConcurrencyStrategy; +import org.springblade.core.boot.resolver.TokenArgumentResolver; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.util.List; + +/** + * WEB配置 + */ +@Slf4j +@Configuration +@EnableCaching +@Order(Ordered.HIGHEST_PRECEDENCE) +public class BladeWebMvcConfiguration implements WebMvcConfigurer { + + @Override + public void addArgumentResolvers(List argumentResolvers) { + argumentResolvers.add(new TokenArgumentResolver()); + } + + @Bean + @ConditionalOnMissingBean + public RequestInterceptor requestInterceptor() { + return new BladeFeignRequestHeaderInterceptor(); + } + + @Bean + public FeignHystrixConcurrencyStrategy feignHystrixConcurrencyStrategy() { + return new FeignHystrixConcurrencyStrategy(); + } + +} diff --git a/blade-core-boot/src/main/java/org/springblade/core/boot/config/MybatisPlusConfiguration.java b/blade-core-boot/src/main/java/org/springblade/core/boot/config/MybatisPlusConfiguration.java new file mode 100644 index 0000000..1ed3dce --- /dev/null +++ b/blade-core-boot/src/main/java/org/springblade/core/boot/config/MybatisPlusConfiguration.java @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.boot.config; + +import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; +import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor; +import com.baomidou.mybatisplus.extension.plugins.PerformanceInterceptor; +import org.mybatis.spring.annotation.MapperScan; +import org.springblade.core.mp.BladeMetaObjectHandler; +import org.springblade.core.launch.constant.AppConstant; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; + +/** + * mybatisplus 配置 + */ +@Configuration +@MapperScan("org.springblade.**.mapper.**") +public class MybatisPlusConfiguration { + + @Bean + public PaginationInterceptor paginationInterceptor() { + return new PaginationInterceptor(); + } + + @Bean + public MetaObjectHandler metaObjectHandler() { + return new BladeMetaObjectHandler(); + } + + /** + * SQL执行效率插件 + */ + @Bean + @Profile({AppConstant.DEV_CDOE, AppConstant.TEST_CODE}) + public PerformanceInterceptor performanceInterceptor() { + return new PerformanceInterceptor(); + } + +} + diff --git a/blade-core-boot/src/main/java/org/springblade/core/boot/config/RetryConfiguration.java b/blade-core-boot/src/main/java/org/springblade/core/boot/config/RetryConfiguration.java new file mode 100644 index 0000000..bd825b5 --- /dev/null +++ b/blade-core-boot/src/main/java/org/springblade/core/boot/config/RetryConfiguration.java @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.boot.config; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.retry.interceptor.RetryInterceptorBuilder; +import org.springframework.retry.interceptor.RetryOperationsInterceptor; + +/** + * 重试机制 + */ +@Slf4j +@Configuration +public class RetryConfiguration { + + @Bean + @ConditionalOnMissingBean(name = "configServerRetryInterceptor") + public RetryOperationsInterceptor configServerRetryInterceptor() { + log.info(String.format( + "configServerRetryInterceptor: Changing backOffOptions " + + "to initial: %s, multiplier: %s, maxInterval: %s", + 1000, 1.2, 5000)); + return RetryInterceptorBuilder + .stateless() + .backOffOptions(1000, 1.2, 5000) + .maxAttempts(10) + .build(); + } + +} diff --git a/blade-core-boot/src/main/java/org/springblade/core/boot/ctrl/BladeController.java b/blade-core-boot/src/main/java/org/springblade/core/boot/ctrl/BladeController.java new file mode 100644 index 0000000..e4db3c0 --- /dev/null +++ b/blade-core-boot/src/main/java/org/springblade/core/boot/ctrl/BladeController.java @@ -0,0 +1,209 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.boot.ctrl; + +import org.springblade.core.boot.file.BladeFile; +import org.springblade.core.boot.file.BladeFileUtil; +import org.springblade.core.secure.BladeUser; +import org.springblade.core.secure.utils.SecureUtil; +import org.springblade.core.tool.api.R; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.propertyeditors.CustomDateEditor; +import org.springframework.web.bind.ServletRequestDataBinder; +import org.springframework.web.bind.annotation.InitBinder; +import org.springframework.web.multipart.MultipartFile; + +import javax.servlet.http.HttpServletRequest; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; + +/** + * Blade控制器封装类 + */ +public class BladeController { + + /** + * ============================ BINDER ================================================= + */ + + @InitBinder + protected void initBinder(ServletRequestDataBinder binder) { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + dateFormat.setLenient(false); + binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, false)); + } + + /** + * ============================ REQUEST ================================================= + */ + + @Autowired + private HttpServletRequest request; + + /** + * 获取request + */ + public HttpServletRequest getRequest() { + return this.request; + } + + /** + * 获取当前用户 + * + * @return + */ + public BladeUser getUser() { + return SecureUtil.getUser(request); + } + + /** ============================ API_RESULT ================================================= */ + + /** + * 返回ApiResult + * + * @param data + * @return R + */ + public R data(T data) { + return R.data(data); + } + + /** + * 返回ApiResult + * + * @param data + * @param message + * @return R + */ + public R data(T data, String message) { + return R.data(data, message); + } + + /** + * 返回ApiResult + * + * @param data + * @param message + * @param code + * @return R + */ + public R data(T data, String message, int code) { + return R.data(code, data, message); + } + + /** + * 返回ApiResult + * + * @param message + * @return R + */ + public R success(String message) { + return R.success(message); + } + + /** + * 返回ApiResult + * + * @param message + * @return R + */ + public R failure(String message) { + return R.failure(message); + } + + /** + * 返回ApiResult + * + * @param flag + * @return R + */ + public R status(boolean flag) { + return R.status(flag); + } + + + /**============================ FILE ================================================= */ + + /** + * 获取BladeFile封装类 + * + * @param file + * @return + */ + public BladeFile getFile(MultipartFile file) { + return BladeFileUtil.getFile(file); + } + + /** + * 获取BladeFile封装类 + * + * @param file + * @param dir + * @return + */ + public BladeFile getFile(MultipartFile file, String dir) { + return BladeFileUtil.getFile(file, dir); + } + + /** + * 获取BladeFile封装类 + * + * @param file + * @param dir + * @param path + * @param virtualPath + * @return + */ + public BladeFile getFile(MultipartFile file, String dir, String path, String virtualPath) { + return BladeFileUtil.getFile(file, dir, path, virtualPath); + } + + /** + * 获取BladeFile封装类 + * + * @param files + * @return + */ + public List getFiles(List files) { + return BladeFileUtil.getFiles(files); + } + + /** + * 获取BladeFile封装类 + * + * @param files + * @param dir + * @return + */ + public List getFiles(List files, String dir) { + return BladeFileUtil.getFiles(files, dir); + } + + /** + * 获取BladeFile封装类 + * + * @param files + * @param path + * @param virtualPath + * @return + */ + public List getFiles(List files, String dir, String path, String virtualPath) { + return BladeFileUtil.getFiles(files, dir, path, virtualPath); + } + + +} diff --git a/blade-core-boot/src/main/java/org/springblade/core/boot/feign/BladeFeignRequestHeaderInterceptor.java b/blade-core-boot/src/main/java/org/springblade/core/boot/feign/BladeFeignRequestHeaderInterceptor.java new file mode 100644 index 0000000..b1168df --- /dev/null +++ b/blade-core-boot/src/main/java/org/springblade/core/boot/feign/BladeFeignRequestHeaderInterceptor.java @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.boot.feign; + +import feign.RequestInterceptor; +import feign.RequestTemplate; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import javax.servlet.http.HttpServletRequest; +import java.util.Enumeration; + +/** + * feign 传递Request header + */ +@Slf4j +public class BladeFeignRequestHeaderInterceptor implements RequestInterceptor { + + @Override + public void apply(RequestTemplate requestTemplate) { + ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + if (attrs != null) { + HttpServletRequest request = attrs.getRequest(); + Enumeration headerNames = request.getHeaderNames(); + if (headerNames != null) { + while (headerNames.hasMoreElements()) { + String name = headerNames.nextElement(); + String value = request.getHeader(name); + /** + * 遍历请求头里面的属性字段,将Authorization添加到新的请求头中转发到下游服务 + * */ + if ("Authorization".equals(name)) { + log.debug("添加自定义请求头key:" + name + ",value:" + value); + requestTemplate.header(name, value); + } + } + } else { + log.warn("FeignHeadConfiguration", "获取请求头失败!"); + } + } + } + +} diff --git a/blade-core-boot/src/main/java/org/springblade/core/boot/feign/FeignHystrixConcurrencyStrategy.java b/blade-core-boot/src/main/java/org/springblade/core/boot/feign/FeignHystrixConcurrencyStrategy.java new file mode 100644 index 0000000..5db1400 --- /dev/null +++ b/blade-core-boot/src/main/java/org/springblade/core/boot/feign/FeignHystrixConcurrencyStrategy.java @@ -0,0 +1,131 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.boot.feign; + +import com.netflix.hystrix.HystrixThreadPoolKey; +import com.netflix.hystrix.HystrixThreadPoolProperties; +import com.netflix.hystrix.strategy.HystrixPlugins; +import com.netflix.hystrix.strategy.concurrency.HystrixConcurrencyStrategy; +import com.netflix.hystrix.strategy.concurrency.HystrixRequestVariable; +import com.netflix.hystrix.strategy.concurrency.HystrixRequestVariableLifecycle; +import com.netflix.hystrix.strategy.eventnotifier.HystrixEventNotifier; +import com.netflix.hystrix.strategy.executionhook.HystrixCommandExecutionHook; +import com.netflix.hystrix.strategy.metrics.HystrixMetricsPublisher; +import com.netflix.hystrix.strategy.properties.HystrixPropertiesStrategy; +import com.netflix.hystrix.strategy.properties.HystrixProperty; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.Callable; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +/** + * 自定义Feign的隔离策略 + */ +public class FeignHystrixConcurrencyStrategy extends HystrixConcurrencyStrategy { + + private static final Logger log = LoggerFactory.getLogger(FeignHystrixConcurrencyStrategy.class); + private HystrixConcurrencyStrategy delegate; + + public FeignHystrixConcurrencyStrategy() { + try { + this.delegate = HystrixPlugins.getInstance().getConcurrencyStrategy(); + if (this.delegate instanceof FeignHystrixConcurrencyStrategy) { + // Welcome to singleton hell... + return; + } + HystrixCommandExecutionHook commandExecutionHook = + HystrixPlugins.getInstance().getCommandExecutionHook(); + HystrixEventNotifier eventNotifier = HystrixPlugins.getInstance().getEventNotifier(); + HystrixMetricsPublisher metricsPublisher = HystrixPlugins.getInstance().getMetricsPublisher(); + HystrixPropertiesStrategy propertiesStrategy = + HystrixPlugins.getInstance().getPropertiesStrategy(); + this.logCurrentStateOfHystrixPlugins(eventNotifier, metricsPublisher, propertiesStrategy); + HystrixPlugins.reset(); + HystrixPlugins.getInstance().registerConcurrencyStrategy(this); + HystrixPlugins.getInstance().registerCommandExecutionHook(commandExecutionHook); + HystrixPlugins.getInstance().registerEventNotifier(eventNotifier); + HystrixPlugins.getInstance().registerMetricsPublisher(metricsPublisher); + HystrixPlugins.getInstance().registerPropertiesStrategy(propertiesStrategy); + } catch (Exception e) { + log.error("Failed to register Sleuth Hystrix Concurrency Strategy", e); + } + } + + private void logCurrentStateOfHystrixPlugins(HystrixEventNotifier eventNotifier, + HystrixMetricsPublisher metricsPublisher, HystrixPropertiesStrategy propertiesStrategy) { + if (log.isDebugEnabled()) { + log.debug("Current Hystrix plugins configuration is [" + "concurrencyStrategy [" + + this.delegate + "]," + "eventNotifier [" + eventNotifier + "]," + "metricPublisher [" + + metricsPublisher + "]," + "propertiesStrategy [" + propertiesStrategy + "]," + "]"); + log.debug("Registering Sleuth Hystrix Concurrency Strategy."); + } + } + + @Override + public Callable wrapCallable(Callable callable) { + RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); + return new WrappedCallable<>(callable, requestAttributes); + } + + @Override + public ThreadPoolExecutor getThreadPool(HystrixThreadPoolKey threadPoolKey, + HystrixProperty corePoolSize, HystrixProperty maximumPoolSize, + HystrixProperty keepAliveTime, TimeUnit unit, BlockingQueue workQueue) { + return this.delegate.getThreadPool(threadPoolKey, corePoolSize, maximumPoolSize, keepAliveTime, + unit, workQueue); + } + + @Override + public ThreadPoolExecutor getThreadPool(HystrixThreadPoolKey threadPoolKey, + HystrixThreadPoolProperties threadPoolProperties) { + return this.delegate.getThreadPool(threadPoolKey, threadPoolProperties); + } + + @Override + public BlockingQueue getBlockingQueue(int maxQueueSize) { + return this.delegate.getBlockingQueue(maxQueueSize); + } + + @Override + public HystrixRequestVariable getRequestVariable(HystrixRequestVariableLifecycle rv) { + return this.delegate.getRequestVariable(rv); + } + + static class WrappedCallable implements Callable { + private final Callable target; + private final RequestAttributes requestAttributes; + + public WrappedCallable(Callable target, RequestAttributes requestAttributes) { + this.target = target; + this.requestAttributes = requestAttributes; + } + + @Override + public T call() throws Exception { + try { + RequestContextHolder.setRequestAttributes(requestAttributes); + return target.call(); + } finally { + RequestContextHolder.resetRequestAttributes(); + } + } + } +} diff --git a/blade-core-boot/src/main/java/org/springblade/core/boot/file/BladeFile.java b/blade-core-boot/src/main/java/org/springblade/core/boot/file/BladeFile.java new file mode 100644 index 0000000..429f242 --- /dev/null +++ b/blade-core-boot/src/main/java/org/springblade/core/boot/file/BladeFile.java @@ -0,0 +1,171 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.boot.file; + +import org.springblade.core.tool.constant.SystemConstant; +import org.springblade.core.tool.date.DateUtil; +import org.springframework.web.multipart.MultipartFile; + +import java.io.File; +import java.io.IOException; +import java.util.Date; + +public class BladeFile { + /** + * 上传文件在附件表中的id + */ + private Object fileId; + + /** + * 上传文件 + */ + private MultipartFile file; + + /** + * 上传分类文件夹 + */ + private String dir; + + /** + * 上传物理路径 + */ + private String uploadPath; + + /** + * 上传虚拟路径 + */ + private String uploadVirtualPath; + + /** + * 文件名 + */ + private String fileName; + + /** + * 真实文件名 + */ + private String originalFileName; + + public BladeFile() { + + } + + public BladeFile(MultipartFile file, String dir) { + this.dir = dir; + this.file = file; + this.fileName = file.getName(); + this.originalFileName = file.getOriginalFilename(); + this.uploadPath = BladeFileUtil.formatUrl(File.separator + SystemConstant.me().getUploadRealPath() + File.separator + dir + File.separator + DateUtil.format(new Date(), "yyyyMMdd") + File.separator + this.originalFileName); + this.uploadVirtualPath = BladeFileUtil.formatUrl(SystemConstant.me().getUploadCtxPath().replace(SystemConstant.me().getContextPath(), "") + File.separator + dir + File.separator + DateUtil.format(new Date(), "yyyyMMdd") + File.separator + this.originalFileName); + } + + public BladeFile(MultipartFile file, String dir, String uploadPath, String uploadVirtualPath) { + this(file, dir); + if (null != uploadPath){ + this.uploadPath = BladeFileUtil.formatUrl(uploadPath); + this.uploadVirtualPath = BladeFileUtil.formatUrl(uploadVirtualPath); + } + } + + /** + * 图片上传 + */ + public void transfer() { + transfer(SystemConstant.me().isCompress()); + } + + /** + * 图片上传 + * @param compress 是否压缩 + */ + public void transfer(boolean compress) { + IFileProxy fileFactory = FileProxyManager.me().getDefaultFileProxyFactory(); + this.transfer(fileFactory, compress); + } + + /** + * 图片上传 + * @param fileFactory 文件上传工厂类 + * @param compress 是否压缩 + */ + public void transfer(IFileProxy fileFactory, boolean compress) { + try { + File file = new File(uploadPath); + + if(null != fileFactory){ + String [] path = fileFactory.path(file, dir); + this.uploadPath = path[0]; + this.uploadVirtualPath = path[1]; + file = fileFactory.rename(file, path[0]); + } + + File pfile = file.getParentFile(); + if (!pfile.exists()) { + pfile.mkdirs(); + } + + this.file.transferTo(file); + + if (compress) { + fileFactory.compress(this.uploadPath); + } + + } catch (IllegalStateException | IOException e) { + e.printStackTrace(); + } + } + + public MultipartFile getFile() { + return file; + } + + public void setFile(MultipartFile file) { + this.file = file; + } + + public String getUploadPath() { + return uploadPath; + } + + public void setUploadPath(String uploadPath) { + this.uploadPath = uploadPath; + } + + public String getUploadVirtualPath() { + return uploadVirtualPath; + } + + public void setUploadVirtualPath(String uploadVirtualPath) { + this.uploadVirtualPath = uploadVirtualPath; + } + + public String getFileName() { + return fileName; + } + + public void setFileName(String fileName) { + this.fileName = fileName; + } + + public String getOriginalFileName() { + return originalFileName; + } + + public void setOriginalFileName(String originalFileName) { + this.originalFileName = originalFileName; + } + +} diff --git a/blade-core-boot/src/main/java/org/springblade/core/boot/file/BladeFileProxyFactory.java b/blade-core-boot/src/main/java/org/springblade/core/boot/file/BladeFileProxyFactory.java new file mode 100644 index 0000000..2f5be0f --- /dev/null +++ b/blade-core-boot/src/main/java/org/springblade/core/boot/file/BladeFileProxyFactory.java @@ -0,0 +1,91 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.boot.file; + +import org.springblade.core.tool.constant.SystemConstant; +import org.springblade.core.tool.date.DateUtil; +import org.springblade.core.tool.utils.ImageUtil; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.util.Date; + +public class BladeFileProxyFactory implements IFileProxy { + + @Override + public File rename(File f, String path) { + File dest = new File(path); + f.renameTo(dest); + return dest; + } + + @Override + public String [] path(File f, String dir) { + //避免网络延迟导致时间不同步 + long time = System.nanoTime(); + + StringBuilder uploadPath = new StringBuilder() + .append(getFileDir(dir, SystemConstant.me().getUploadRealPath())) + .append(time) + .append(getFileExt(f.getName())); + + StringBuilder virtualPath = new StringBuilder() + .append(getFileDir(dir, SystemConstant.me().getUploadCtxPath())) + .append(time) + .append(getFileExt(f.getName())); + + return new String [] {BladeFileUtil.formatUrl(uploadPath.toString()), BladeFileUtil.formatUrl(virtualPath.toString())}; + } + + /** + * 获取文件后缀 + */ + public static String getFileExt(String fileName) { + if (fileName.indexOf(".") == -1) + return ".jpg"; + else + return fileName.substring(fileName.lastIndexOf('.'), fileName.length()); + } + + /** + * 获取文件保存地址 + * @param saveDir + * @return + */ + public static String getFileDir(String dir, String saveDir) { + StringBuilder newFileDir = new StringBuilder(); + newFileDir.append(saveDir) + .append(File.separator).append(dir).append(File.separator).append(DateUtil.format(new Date(), "yyyyMMdd")) + .append(File.separator); + return newFileDir.toString(); + } + + + /** + * 图片压缩 + * @param path 文件地址 + * @return + */ + public void compress(String path) { + try { + ImageUtil.zoomScale(ImageUtil.readImage(path), new FileOutputStream(new File(path)), null, SystemConstant.me().getCompressScale(), SystemConstant.me().isCompressFlag()); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } + } + +} diff --git a/blade-core-boot/src/main/java/org/springblade/core/boot/file/BladeFileUtil.java b/blade-core-boot/src/main/java/org/springblade/core/boot/file/BladeFileUtil.java new file mode 100644 index 0000000..e1aa2f2 --- /dev/null +++ b/blade-core-boot/src/main/java/org/springblade/core/boot/file/BladeFileUtil.java @@ -0,0 +1,200 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.boot.file; + +import org.springblade.core.tool.utils.StringUtil; +import org.springframework.web.multipart.MultipartFile; + +import java.util.*; + +public class BladeFileUtil { + + // 定义允许上传的文件扩展名 + private static HashMap extMap = new HashMap(); + // 图片扩展名 + private static String[] fileTypes = new String[] { "gif", "jpg", "jpeg", "png", "bmp" }; + + static { + extMap.put("image", ".gif,.jpg,.jpeg,.png,.bmp,.JPG,.JPEG,.PNG"); + extMap.put("flash", ".swf,.flv"); + extMap.put("media", ".swf,.flv,.mp3,.mp4,.wav,.wma,.wmv,.mid,.avi,.mpg,.asf,.rm,.rmvb"); + extMap.put("file", ".doc,.docx,.xls,.xlsx,.ppt,.htm,.html,.txt,.zip,.rar,.gz,.bz2"); + extMap.put("allfile", ".gif,.jpg,.jpeg,.png,.bmp,.swf,.flv,.mp3,.mp4,.wav,.wma,.wmv,.mid,.avi,.mpg,.asf,.rm,.rmvb,.doc,.docx,.xls,.xlsx,.ppt,.htm,.html,.txt,.zip,.rar,.gz,.bz2"); + } + + /** + * 获取文件后缀 + * + * @param @param fileName + * @param @return 设定文件 + * @return String 返回类型 + */ + public static String getFileExt(String fileName) { + return fileName.substring(fileName.lastIndexOf('.'), fileName.length()); + } + + /** + * 测试文件后缀 只让指定后缀的文件上传,像jsp,war,sh等危险的后缀禁止 + * + * @return + */ + public static boolean testExt(String dir, String fileName) { + String fileExt = getFileExt(fileName); + + String ext = extMap.get(dir); + if (StringUtil.isBlank(ext) || ext.indexOf(fileExt) == -1) { + return false; + } + return true; + } + + /** + * 文件管理排序 + */ + public enum FileSort { + size, type, name; + + // 文本排序转换成枚举 + public static FileSort of(String sort) { + try { + return FileSort.valueOf(sort); + } catch (Exception e) { + return FileSort.name; + } + } + } + + public static class NameComparator implements Comparator { + public int compare(Object a, Object b) { + Hashtable hashA = (Hashtable) a; + Hashtable hashB = (Hashtable) b; + if (((Boolean) hashA.get("is_dir")) && !((Boolean) hashB.get("is_dir"))) { + return -1; + } else if (!((Boolean) hashA.get("is_dir")) && ((Boolean) hashB.get("is_dir"))) { + return 1; + } else { + return ((String) hashA.get("filename")).compareTo((String) hashB.get("filename")); + } + } + } + + public static class SizeComparator implements Comparator { + public int compare(Object a, Object b) { + Hashtable hashA = (Hashtable) a; + Hashtable hashB = (Hashtable) b; + if (((Boolean) hashA.get("is_dir")) && !((Boolean) hashB.get("is_dir"))) { + return -1; + } else if (!((Boolean) hashA.get("is_dir")) && ((Boolean) hashB.get("is_dir"))) { + return 1; + } else { + if (((Long) hashA.get("filesize")) > ((Long) hashB.get("filesize"))) { + return 1; + } else if (((Long) hashA.get("filesize")) < ((Long) hashB.get("filesize"))) { + return -1; + } else { + return 0; + } + } + } + } + + public static class TypeComparator implements Comparator { + public int compare(Object a, Object b) { + Hashtable hashA = (Hashtable) a; + Hashtable hashB = (Hashtable) b; + if (((Boolean) hashA.get("is_dir")) && !((Boolean) hashB.get("is_dir"))) { + return -1; + } else if (!((Boolean) hashA.get("is_dir")) && ((Boolean) hashB.get("is_dir"))) { + return 1; + } else { + return ((String) hashA.get("filetype")).compareTo((String) hashB.get("filetype")); + } + } + } + + public static String formatUrl(String url) { + return url.replaceAll("\\\\", "/"); + } + + + /********************************BladeFile封装********************************************************/ + + /** + * 获取BladeFile封装类 + * @param file + * @return + */ + public static BladeFile getFile(MultipartFile file){ + return getFile(file, "image", null, null); + } + + /** + * 获取BladeFile封装类 + * @param file + * @param dir + * @return + */ + public static BladeFile getFile(MultipartFile file, String dir){ + return getFile(file, dir, null, null); + } + + /** + * 获取BladeFile封装类 + * @param file + * @param dir + * @param path + * @param virtualPath + * @return + */ + public static BladeFile getFile(MultipartFile file, String dir, String path, String virtualPath){ + return new BladeFile(file, dir, path, virtualPath); + } + + /** + * 获取BladeFile封装类 + * @param files + * @return + */ + public static List getFiles(List files){ + return getFiles(files, "image", null, null); + } + + /** + * 获取BladeFile封装类 + * @param files + * @param dir + * @return + */ + public static List getFiles(List files, String dir){ + return getFiles(files, dir, null, null); + } + + /** + * 获取BladeFile封装类 + * @param files + * @param path + * @param virtualPath + * @return + */ + public static List getFiles(List files, String dir, String path, String virtualPath){ + List list = new ArrayList<>(); + for (MultipartFile file : files){ + list.add(new BladeFile(file, dir, path, virtualPath)); + } + return list; + } + +} diff --git a/blade-core-boot/src/main/java/org/springblade/core/boot/file/FileMaker.java b/blade-core-boot/src/main/java/org/springblade/core/boot/file/FileMaker.java new file mode 100644 index 0000000..81726ee --- /dev/null +++ b/blade-core-boot/src/main/java/org/springblade/core/boot/file/FileMaker.java @@ -0,0 +1,209 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.boot.file; + + +import org.springblade.core.tool.utils.StringUtil; +import org.springblade.core.tool.utils.StringPool; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.*; + +public class FileMaker { + private static final String DEFAULT_CONTENT_TYPE = "application/octet-stream"; + + private File file; + + private String fileName; + + private HttpServletRequest request; + + private HttpServletResponse response; + + public static FileMaker init(HttpServletRequest request, HttpServletResponse response, File file) { + return new FileMaker(request, response, file); + } + + public static FileMaker init(HttpServletRequest request, HttpServletResponse response, File file, String fileName) { + return new FileMaker(request, response, file, fileName); + } + + private FileMaker(HttpServletRequest request, HttpServletResponse response, File file) { + if (file == null) { + throw new IllegalArgumentException("file can not be null."); + } + this.file = file; + this.request = request; + this.response = response; + this.fileName = file.getName(); + } + + private FileMaker(HttpServletRequest request, HttpServletResponse response, File file, String fileName) { + if (file == null) { + throw new IllegalArgumentException("file can not be null."); + } + this.file = file; + this.request = request; + this.response = response; + this.fileName = fileName; + } + + public void start() { + if (file == null || !file.isFile()) { + throw new RuntimeException(); + } + + // --------- + response.setHeader("Accept-Ranges", "bytes"); + response.setHeader("Content-disposition", "attachment; filename=" + encodeFileName(fileName)); + response.setContentType(DEFAULT_CONTENT_TYPE); + + // --------- + if (StringUtil.isBlank(request.getHeader("Range"))) + normalStart(); + else + rangeStart(); + } + + private String encodeFileName(String fileName) { + try { + return new String(fileName.getBytes(StringPool.GBK), StringPool.ISO_8859_1); + } catch (UnsupportedEncodingException e) { + return fileName; + } + } + + private void normalStart() { + response.setHeader("Content-Length", String.valueOf(file.length())); + InputStream inputStream = null; + OutputStream outputStream = null; + try { + inputStream = new BufferedInputStream(new FileInputStream(file)); + outputStream = response.getOutputStream(); + byte[] buffer = new byte[1024]; + for (int len = -1; (len = inputStream.read(buffer)) != -1;) { + outputStream.write(buffer, 0, len); + } + outputStream.flush(); + } + catch (IOException e) { + throw new RuntimeException(e); + } + catch (Exception e) { + throw new RuntimeException(e); + } + finally { + if (inputStream != null) + try {inputStream.close();} catch (IOException e) {} + if (outputStream != null) + try {outputStream.close();} catch (IOException e) {} + } + } + + private void rangeStart() { + Long[] range = {null, null}; + processRange(range); + + String contentLength = String.valueOf(range[1].longValue() - range[0].longValue() + 1); + response.setHeader("Content-Length", contentLength); + response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); // status = 206 + + // Content-Range: bytes 0-499/10000 + StringBuilder contentRange = new StringBuilder("bytes ").append(String.valueOf(range[0])).append("-").append(String.valueOf(range[1])).append("/").append(String.valueOf(file.length())); + response.setHeader("Content-Range", contentRange.toString()); + + InputStream inputStream = null; + OutputStream outputStream = null; + try { + long start = range[0]; + long end = range[1]; + inputStream = new BufferedInputStream(new FileInputStream(file)); + if (inputStream.skip(start) != start) + throw new RuntimeException("File skip error"); + outputStream = response.getOutputStream(); + byte[] buffer = new byte[1024]; + long position = start; + for (int len; position <= end && (len = inputStream.read(buffer)) != -1;) { + if (position + len <= end) { + outputStream.write(buffer, 0, len); + position += len; + } + else { + for (int i=0; i= fileLength) + range[i] = fileLength - 1; + } + } + + // Range format like: 9500- + if (range[0] != null && range[1] == null) { + range[1] = fileLength - 1; + } + // Range format like: -500 + else if (range[0] == null && range[1] != null) { + range[0] = fileLength - range[1]; + range[1] = fileLength - 1; + } + + // check final range + if (range[0] == null || range[1] == null || range[0].longValue() > range[1].longValue()) + throw new RuntimeException("Range error"); + } +} diff --git a/blade-core-boot/src/main/java/org/springblade/core/boot/file/FileProxyManager.java b/blade-core-boot/src/main/java/org/springblade/core/boot/file/FileProxyManager.java new file mode 100644 index 0000000..a284684 --- /dev/null +++ b/blade-core-boot/src/main/java/org/springblade/core/boot/file/FileProxyManager.java @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.boot.file; + +import java.io.File; + +public class FileProxyManager { + private IFileProxy defaultFileProxyFactory = new BladeFileProxyFactory(); + + private static FileProxyManager me = new FileProxyManager(); + + public static FileProxyManager me() { + return me; + } + + public IFileProxy getDefaultFileProxyFactory() { + return defaultFileProxyFactory; + } + + public void setDefaultFileProxyFactory(IFileProxy defaultFileProxyFactory) { + this.defaultFileProxyFactory = defaultFileProxyFactory; + } + + public String[] path(File file, String dir) { + return defaultFileProxyFactory.path(file, dir); + } + + public File rename(File file, String path) { + return defaultFileProxyFactory.rename(file, path); + } + +} diff --git a/blade-core-boot/src/main/java/org/springblade/core/boot/file/IFileProxy.java b/blade-core-boot/src/main/java/org/springblade/core/boot/file/IFileProxy.java new file mode 100644 index 0000000..2162cbb --- /dev/null +++ b/blade-core-boot/src/main/java/org/springblade/core/boot/file/IFileProxy.java @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.boot.file; + +import java.io.File; + +public interface IFileProxy { + + /** + * 返回路径[物理路径][虚拟路径] + * @param file + * @param dir + * @return + */ + String [] path(File file, String dir); + + /** + * 文件重命名策略 + * @param file + * @param path + * @return + */ + File rename(File file, String path); + + /** + * 图片压缩 + */ + void compress(String path); + +} diff --git a/blade-core-boot/src/main/java/org/springblade/core/boot/logger/RequestLogAspect.java b/blade-core-boot/src/main/java/org/springblade/core/boot/logger/RequestLogAspect.java new file mode 100644 index 0000000..aec6faa --- /dev/null +++ b/blade-core-boot/src/main/java/org/springblade/core/boot/logger/RequestLogAspect.java @@ -0,0 +1,132 @@ +package org.springblade.core.boot.logger; + +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.reflect.MethodSignature; +import org.springblade.core.tool.jackson.JsonUtil; +import org.springblade.core.tool.utils.BeanUtil; +import org.springblade.core.tool.utils.ClassUtil; +import org.springblade.core.tool.utils.StringUtil; +import org.springblade.core.tool.utils.WebUtil; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.core.MethodParameter; +import org.springframework.core.io.InputStreamSource; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.multipart.MultipartFile; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.InputStream; +import java.lang.reflect.Method; +import java.util.*; +import java.util.concurrent.TimeUnit; + +/** + * Spring boot 控制器 请求日志,方便代码调试 + * + * @author L.cm + */ +@Slf4j +@Aspect +@Configuration +@Profile({"dev", "test"}) +public class RequestLogAspect { + + /** + * AOP 环切 控制器 R 返回值 + * @param point JoinPoint + * @throws Throwable 异常 + * @return Object + */ + @Around( + "execution(!static org.springblade.core.tool.api.R<*> *(..)) && " + + "(@within(org.springframework.stereotype.Controller) || " + + "@within(org.springframework.web.bind.annotation.RestController))" + ) + public Object aroundApi(ProceedingJoinPoint point) throws Throwable { + MethodSignature ms = (MethodSignature) point.getSignature(); + Method method = ms.getMethod(); + Object[] args = point.getArgs(); + // 请求参数处理 + final Map paraMap = new HashMap<>(16); + for (int i = 0; i < args.length; i++) { + // 读取方法参数 + MethodParameter methodParam = ClassUtil.getMethodParameter(method, i); + // PathVariable 参数跳过 + PathVariable pathVariable = methodParam.getParameterAnnotation(PathVariable.class); + if (pathVariable != null) { + continue; + } + RequestBody requestBody = methodParam.getParameterAnnotation(RequestBody.class); + Object object = args[i]; + // 如果是body的json则是对象 + if (requestBody != null && object != null) { + paraMap.putAll(BeanUtil.toMap(object)); + } else { + // 参数名 + RequestParam requestParam = methodParam.getParameterAnnotation(RequestParam.class); + String paraName; + if (requestParam != null && StringUtil.isNotBlank(requestParam.value())) { + paraName = requestParam.value(); + } else { + paraName = methodParam.getParameterName(); + } + paraMap.put(paraName, object); + } + } + HttpServletRequest request = WebUtil.getRequest(); + String requestURI = request.getRequestURI(); + String requestMethod = request.getMethod(); + // 处理 参数 + List needRemoveKeys = new ArrayList<>(paraMap.size()); + paraMap.forEach((key, value) -> { + if (value instanceof HttpServletRequest) { + needRemoveKeys.add(key); + paraMap.putAll(((HttpServletRequest) value).getParameterMap()); + } else if (value instanceof HttpServletResponse) { + needRemoveKeys.add(key); + } else if (value instanceof InputStream) { + needRemoveKeys.add(key); + } else if (value instanceof MultipartFile) { + String fileName = ((MultipartFile) value).getOriginalFilename(); + paraMap.put(key, fileName); + } else if (value instanceof InputStreamSource) { + needRemoveKeys.add(key); + } else if (value instanceof WebRequest) { + needRemoveKeys.add(key); + paraMap.putAll(((WebRequest) value).getParameterMap()); + } + }); + needRemoveKeys.forEach(paraMap::remove); + // 打印请求 + if (paraMap.isEmpty()) { + log.info("===> {}: {}", requestMethod, requestURI); + } else { + log.info("===> {}: {} Parameters: {}", requestMethod, requestURI, JsonUtil.toJson(paraMap)); + } + // 打印请求头 + Enumeration headers = request.getHeaderNames(); + while (headers.hasMoreElements()) { + String headerName = headers.nextElement(); + String headerValue = request.getHeader(headerName); + log.info("===headers=== {} : {}", headerName, headerValue); + } + // 打印执行时间 + long startNs = System.nanoTime(); + try { + Object result = point.proceed(); + log.info("===Result=== {}", JsonUtil.toJson(result)); + return result; + } finally { + long tookMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs); + log.info("<=== {}: {} ({} ms)", request.getMethod(), requestURI, tookMs); + } + } + +} diff --git a/blade-core-boot/src/main/java/org/springblade/core/boot/node/BaseNode.java b/blade-core-boot/src/main/java/org/springblade/core/boot/node/BaseNode.java new file mode 100644 index 0000000..bf2b221 --- /dev/null +++ b/blade-core-boot/src/main/java/org/springblade/core/boot/node/BaseNode.java @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.boot.node; + +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +/** + * 节点基类 + * + * @author zhuangqian + */ +@Data +public class BaseNode implements INode { + + protected Integer id;//主键ID + protected Integer parentId;//父节点ID + protected List children = new ArrayList<>();//子孙节点 + +} diff --git a/blade-core-boot/src/main/java/org/springblade/core/boot/node/ForestNode.java b/blade-core-boot/src/main/java/org/springblade/core/boot/node/ForestNode.java new file mode 100644 index 0000000..4c802b4 --- /dev/null +++ b/blade-core-boot/src/main/java/org/springblade/core/boot/node/ForestNode.java @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.boot.node; + +import lombok.Data; +import lombok.EqualsAndHashCode; + + +/** + * 森林节点类 + */ +@Data +@EqualsAndHashCode(callSuper = false) +public class ForestNode extends BaseNode { + + private Object content;//节点内容 + + public ForestNode(Integer id, Integer parentId, Object content) { + this.id = id; + this.parentId = parentId; + this.content = content; + } + +} diff --git a/blade-core-boot/src/main/java/org/springblade/core/boot/node/ForestNodeManager.java b/blade-core-boot/src/main/java/org/springblade/core/boot/node/ForestNodeManager.java new file mode 100644 index 0000000..49a9517 --- /dev/null +++ b/blade-core-boot/src/main/java/org/springblade/core/boot/node/ForestNodeManager.java @@ -0,0 +1,62 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.boot.node; + +import java.util.ArrayList; +import java.util.List; + +/** + * 森林管理类 + * + * @author zhuangqian + */ +public class ForestNodeManager { + + private List list;// 森林的所有节点 + + public ForestNodeManager(List items) { + list = items; + } + + /** + * 根据节点ID获取一个节点 + * + * @param id 节点ID + * @return 对应的节点对象 + */ + public INode getTreeNodeAT(int id) { + for (INode forestNode : list) { + if (forestNode.getId() == id) + return forestNode; + } + return null; + } + + /** + * 获取树的根节点(一个森林对应多颗树) + * + * @return 树的根节点集合 + */ + public List getRoot() { + List roots = new ArrayList<>(); + for (INode forestNode : list) { + if (forestNode.getParentId() == 0) + roots.add(forestNode); + } + return roots; + } + +} diff --git a/blade-core-boot/src/main/java/org/springblade/core/boot/node/ForestNodeMerger.java b/blade-core-boot/src/main/java/org/springblade/core/boot/node/ForestNodeMerger.java new file mode 100644 index 0000000..c0c6230 --- /dev/null +++ b/blade-core-boot/src/main/java/org/springblade/core/boot/node/ForestNodeMerger.java @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.boot.node; + +import java.util.List; + +/** + * 森林节点归并类 + * + * @author zhuangqian + */ +public class ForestNodeMerger { + + /** + * 将节点数组归并为一个森林(多棵树)(填充节点的children域) + * 时间复杂度为O(n^2) + * + * @param items 节点域 + * @return 多棵树的根节点集合 + */ + public static List merge(List items) { + ForestNodeManager forestNodeManager = new ForestNodeManager(items); + for (INode forestNode : items) { + if (forestNode.getParentId() != 0) { + INode node = forestNodeManager.getTreeNodeAT(forestNode.getParentId()); + node.getChildren().add(forestNode); + } + } + return forestNodeManager.getRoot(); + } + +} diff --git a/blade-core-boot/src/main/java/org/springblade/core/boot/node/INode.java b/blade-core-boot/src/main/java/org/springblade/core/boot/node/INode.java new file mode 100644 index 0000000..14fd32e --- /dev/null +++ b/blade-core-boot/src/main/java/org/springblade/core/boot/node/INode.java @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.boot.node; + +import java.util.List; + +/** + * Created by Blade. + * + * @author zhuangqian + */ +public interface INode { + + Integer getId(); + + Integer getParentId(); + + List getChildren(); + +} diff --git a/blade-core-boot/src/main/java/org/springblade/core/boot/node/NodeTest.java b/blade-core-boot/src/main/java/org/springblade/core/boot/node/NodeTest.java new file mode 100644 index 0000000..fd8b904 --- /dev/null +++ b/blade-core-boot/src/main/java/org/springblade/core/boot/node/NodeTest.java @@ -0,0 +1,34 @@ +package org.springblade.core.boot.node; + +import org.springblade.core.tool.jackson.JsonUtil; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by Blade. + * + * @author zhuangqian + */ +public class NodeTest { + + public static void main(String[] args) { + List list = new ArrayList<>(); + list.add(new ForestNode(1, 0, "1")); + list.add(new ForestNode(2, 0, "2")); + list.add(new ForestNode(3, 1, "3")); + list.add(new ForestNode(4, 2, "4")); + list.add(new ForestNode(5, 3, "5")); + list.add(new ForestNode(6, 4, "6")); + list.add(new ForestNode(7, 3, "7")); + list.add(new ForestNode(8, 5, "8")); + list.add(new ForestNode(9, 6, "9")); + list.add(new ForestNode(10, 9, "10")); + List tns = ForestNodeMerger.merge(list); + tns.forEach(node -> { + ForestNode n = (ForestNode) node; + System.out.println(JsonUtil.toJson(n)); + }); + } + +} diff --git a/blade-core-boot/src/main/java/org/springblade/core/boot/resolver/TokenArgumentResolver.java b/blade-core-boot/src/main/java/org/springblade/core/boot/resolver/TokenArgumentResolver.java new file mode 100644 index 0000000..49535ec --- /dev/null +++ b/blade-core-boot/src/main/java/org/springblade/core/boot/resolver/TokenArgumentResolver.java @@ -0,0 +1,63 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.boot.resolver; + +import lombok.extern.slf4j.Slf4j; +import org.springblade.core.secure.BladeUser; +import org.springblade.core.secure.utils.SecureUtil; +import org.springframework.core.MethodParameter; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +import javax.servlet.http.HttpServletRequest; + +/** + * Token转化BladeUser + */ +@Slf4j +public class TokenArgumentResolver implements HandlerMethodArgumentResolver { + + /** + * 1. 入参筛选 + * + * @param methodParameter 参数集合 + * @return 格式化后的参数 + */ + @Override + public boolean supportsParameter(MethodParameter methodParameter) { + return methodParameter.getParameterType().equals(BladeUser.class); + } + + /** + * @param methodParameter 入参集合 + * @param modelAndViewContainer model 和 view + * @param nativeWebRequest web相关 + * @param webDataBinderFactory 入参解析 + * @return 包装对象 + * @throws Exception exception + */ + @Override + public Object resolveArgument(MethodParameter methodParameter, + ModelAndViewContainer modelAndViewContainer, + NativeWebRequest nativeWebRequest, + WebDataBinderFactory webDataBinderFactory) { + HttpServletRequest request = nativeWebRequest.getNativeRequest(HttpServletRequest.class); + return SecureUtil.getUser(request); + } + +} diff --git a/blade-core-boot/src/main/resources/META-INF/spring-devtools.properties b/blade-core-boot/src/main/resources/META-INF/spring-devtools.properties new file mode 100644 index 0000000..785b4fb --- /dev/null +++ b/blade-core-boot/src/main/resources/META-INF/spring-devtools.properties @@ -0,0 +1 @@ +restart.include.blade-core-boot=/blade-core-boot[\\w-]+\.jar diff --git a/blade-core-boot/src/main/resources/META-INF/spring.factories b/blade-core-boot/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..6051062 --- /dev/null +++ b/blade-core-boot/src/main/resources/META-INF/spring.factories @@ -0,0 +1,6 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + org.springblade.core.boot.config.MybatisPlusConfiguration,\ + org.springblade.core.boot.logger.RequestLogAspect,\ + org.springblade.core.boot.config.RetryConfiguration,\ + org.springblade.core.boot.config.BladeWebMvcConfiguration,\ + org.springblade.core.boot.config.BladeBootAutoConfiguration diff --git a/blade-core-boot/src/main/resources/banner.txt b/blade-core-boot/src/main/resources/banner.txt new file mode 100644 index 0000000..cbd0280 --- /dev/null +++ b/blade-core-boot/src/main/resources/banner.txt @@ -0,0 +1,10 @@ +${AnsiColor.BRIGHT_CYAN} _____ _ ${AnsiColor.BLUE} ______ _ _ +${AnsiColor.BRIGHT_CYAN}/ ___| (_) ${AnsiColor.BLUE} | ___ \| | | | +${AnsiColor.BRIGHT_CYAN}\ `--. _ __ _ __ _ _ __ __ _ ${AnsiColor.BLUE} | |_/ /| | __ _ __| | ___ +${AnsiColor.BRIGHT_CYAN} `--. \| '_ \ | '__|| || '_ \ / _` | ${AnsiColor.BLUE} | ___ \| | / _` | / _` | / _ \ +${AnsiColor.BRIGHT_CYAN}/\__/ /| |_) || | | || | | || (_| | ${AnsiColor.BLUE} | |_/ /| || (_| || (_| || __/ +${AnsiColor.BRIGHT_CYAN}\____/ | .__/ |_| |_||_| |_| \__, | ${AnsiColor.BLUE} \____/ |_| \__,_| \__,_| \___| +${AnsiColor.BRIGHT_CYAN} | | __/ | +${AnsiColor.BRIGHT_CYAN} |_| |___/ + +${AnsiColor.BLUE}:: SpringBlade :: ${spring.application.name}:${AnsiColor.RED}${blade.env}${AnsiColor.BLUE} :: Running SpringBoot ${spring-boot.version} :: ${AnsiColor.BRIGHT_BLACK} diff --git a/blade-core-boot/src/main/resources/bootstrap.yml b/blade-core-boot/src/main/resources/bootstrap.yml new file mode 100644 index 0000000..5f3e55a --- /dev/null +++ b/blade-core-boot/src/main/resources/bootstrap.yml @@ -0,0 +1,87 @@ +#服务器配置 +server: + undertow: + # 设置IO线程数, 它主要执行非阻塞的任务,它们会负责多个连接, 默认设置每个CPU核心一个线程 + io-threads: 4 + # 阻塞任务线程池, 当执行类似servlet请求阻塞操作, undertow会从这个线程池中取得线程,它的值设置取决于系统的负载 + worker-threads: 20 + # 以下的配置会影响buffer,这些buffer会用于服务器连接的IO操作,有点类似netty的池化内存管理 + buffer-size: 1024 + # 是否分配的直接内存 + direct-buffers: true + +#spring配置 +spring: + cloud: + config: + label: master + profile: ${blade.env} + fail-fast: true + discovery: + enabled: true + service-id: blade-config-server + cache: + ehcache: + config: classpath:config/ehcache.xml + http: + encoding: + charset: UTF-8 + force: true + servlet: + multipart: + max-file-size: 256MB + max-request-size: 1024MB + mvc: + throw-exception-if-no-handler-found: true + resources: + add-mappings: false + datasource: + driver-class-name: com.mysql.jdbc.Driver + devtools: + restart: + log-condition-evaluation-delta: false + + +#配置日志地址 +logging: + config: classpath:log/logback_${blade.env}.xml + +# mybatis +mybatis-plus: + mapper-locations: classpath:org/springblade/**/mapper/*Mapper.xml + #实体扫描,多个package用逗号或者分号分隔 + typeAliasesPackage: org.springblade.**.model + #typeEnumsPackage: org.springblade.dashboard.entity.enums + global-config: + #刷新mapper 调试神器 + refresh: ${blade.dev-mode} + # 关闭MP3.0自带的banner + banner: false + db-config: + #主键类型 0:"数据库ID自增", 1:"用户输入ID",2:"全局唯一ID (数字类型唯一ID)", 3:"全局唯一ID UUID"; + id-type: 0 + #字段策略 0:"忽略判断",1:"非 NULL 判断"),2:"非空判断" + field-strategy: 2 + #驼峰下划线转换 + column-underline: true + # 逻辑删除配置 + # 逻辑删除全局值(1表示已删除,这也是Mybatis Plus的默认配置) + logic-delete-value: 1 + # 逻辑未删除全局值(0表示未删除,这也是Mybatis Plus的默认配置) + logic-not-delete-value: 0 + configuration: + map-underscore-to-camel-case: true + cache-enabled: false + +#swagger公共信息 +swagger: + title: SpringBlade 接口文档系统 + description: SpringBlade 接口文档系统 + version: 1.0.0 + license: Powered By SpringBlade + licenseUrl: https://springblade.org + terms-of-service-url: https://springblade.org + contact: + name: smallchill + email: smallchill@163.com + url: https://gitee.com/smallc diff --git a/blade-core-boot/src/main/resources/config/ehcache.xml b/blade-core-boot/src/main/resources/config/ehcache.xml new file mode 100644 index 0000000..1623029 --- /dev/null +++ b/blade-core-boot/src/main/resources/config/ehcache.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/blade-core-boot/src/main/resources/log/logback_dev.xml b/blade-core-boot/src/main/resources/log/logback_dev.xml new file mode 100644 index 0000000..4faf6ed --- /dev/null +++ b/blade-core-boot/src/main/resources/log/logback_dev.xml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + ${CONSOLE_LOG_PATTERN} + utf8 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/blade-core-boot/src/main/resources/log/logback_prod.xml b/blade-core-boot/src/main/resources/log/logback_prod.xml new file mode 100644 index 0000000..eb8dc78 --- /dev/null +++ b/blade-core-boot/src/main/resources/log/logback_prod.xml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + ${CONSOLE_LOG_PATTERN} + utf8 + + + + + + + + target/blade/log/info-%d{yyyy-MM-dd}.log + + 365 + + + %n%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%logger{50}] %n%-5level: %msg%n + + + + 500MB + + + + INFO + ACCEPT + DENY + + + + + + + + target/blade/log/error-%d{yyyy-MM-dd}.log + + 365 + + + %n%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%logger{50}] %n%-5level: %msg%n + + + + 500MB + + + + ERROR + ACCEPT + DENY + + + + + + + + + + + + + + diff --git a/blade-core-boot/src/main/resources/log/logback_test.xml b/blade-core-boot/src/main/resources/log/logback_test.xml new file mode 100644 index 0000000..eb8dc78 --- /dev/null +++ b/blade-core-boot/src/main/resources/log/logback_test.xml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + ${CONSOLE_LOG_PATTERN} + utf8 + + + + + + + + target/blade/log/info-%d{yyyy-MM-dd}.log + + 365 + + + %n%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%logger{50}] %n%-5level: %msg%n + + + + 500MB + + + + INFO + ACCEPT + DENY + + + + + + + + target/blade/log/error-%d{yyyy-MM-dd}.log + + 365 + + + %n%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%logger{50}] %n%-5level: %msg%n + + + + 500MB + + + + ERROR + ACCEPT + DENY + + + + + + + + + + + + + + diff --git a/blade-core-boot/src/main/resources/static/favicon.ico b/blade-core-boot/src/main/resources/static/favicon.ico new file mode 100644 index 0000000..5e2aec9 Binary files /dev/null and b/blade-core-boot/src/main/resources/static/favicon.ico differ diff --git a/blade-core-launch/pom.xml b/blade-core-launch/pom.xml new file mode 100644 index 0000000..b0c2956 --- /dev/null +++ b/blade-core-launch/pom.xml @@ -0,0 +1,36 @@ + + + + blade-tool + org.springblade + 1.0.0-RC1 + + + 4.0.0 + + blade-core-launch + ${project.artifactId} + ${blade.tool.version} + jar + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-tomcat + + + + + org.springframework.boot + spring-boot-starter-undertow + + + + diff --git a/blade-core-launch/src/main/java/org/springblade/core/launch/BladeApplication.java b/blade-core-launch/src/main/java/org/springblade/core/launch/BladeApplication.java new file mode 100644 index 0000000..8f28ddf --- /dev/null +++ b/blade-core-launch/src/main/java/org/springblade/core/launch/BladeApplication.java @@ -0,0 +1,109 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.launch; + +import org.springblade.core.launch.constant.AppConstant; +import org.springblade.core.launch.service.LauncherService; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.env.*; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +import java.util.*; +import java.util.function.Function; + +/** + * 项目启动器,搞定环境变量问题 + */ +public class BladeApplication { + + /** + * Create an application context + * + * @param appName application name + * @param source The sources + * @param args args the command line arguments + * @return an application context created from the current state + * @run java -jar app.jar --spring.profiles.active=prod --server.port=2333 + */ + public static ConfigurableApplicationContext run(String appName, Class source, String... args) { + SpringApplicationBuilder builder = createSpringApplicationBuilder(appName, source, args); + return builder.run(args); + } + + private static SpringApplicationBuilder createSpringApplicationBuilder(String appName, Class source, String... args) { + Assert.hasText(appName, "[appName]服务名不能为空"); + // 读取环境变量,使用spring boot的规则 + ConfigurableEnvironment environment = new StandardEnvironment(); + MutablePropertySources propertySources = environment.getPropertySources(); + propertySources.addFirst(new SimpleCommandLinePropertySource(args)); + propertySources.addLast(new MapPropertySource(StandardEnvironment.SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME, environment.getSystemProperties())); + propertySources.addLast(new SystemEnvironmentPropertySource(StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, environment.getSystemEnvironment())); + // 获取配置的环境变量 + String[] activeProfiles = environment.getActiveProfiles(); + // 判断环境:dev、test、prod + List profiles = Arrays.asList(activeProfiles); + // 预设的环境 + List presetProfiles = new ArrayList<>(Arrays.asList(AppConstant.DEV_CDOE, AppConstant.TEST_CODE, AppConstant.PROD_CODE)); + // 交集 + presetProfiles.retainAll(profiles); + // 当前使用 + List activeProfileList = new ArrayList<>(profiles); + Function joinFun = StringUtils::arrayToCommaDelimitedString; + SpringApplicationBuilder builder = new SpringApplicationBuilder(source); + String profile; + if (activeProfileList.isEmpty()) { + // 默认dev开发 + profile = AppConstant.DEV_CDOE; + activeProfileList.add(profile); + builder.profiles(profile); + } else if (activeProfileList.size() == 1) { + profile = activeProfileList.get(0); + } else { + // 同时存在dev、test、prod环境时 + throw new RuntimeException("同时存在环境变量:[" + StringUtils.arrayToCommaDelimitedString(activeProfiles) + "]"); + } + String startJarPath = BladeApplication.class.getResource("/").getPath().split("!")[0]; + String activePros = joinFun.apply(activeProfileList.toArray()); + System.out.println(String.format("----启动中,读取到的环境变量:[%s],jar地址:[%s]----", activePros, startJarPath)); + Properties props = System.getProperties(); + props.setProperty("spring.application.name", appName); + props.setProperty("spring.profiles.active", profile); + props.setProperty("info.version", AppConstant.APPLICATION_VERSION); + props.setProperty("info.desc", appName); + props.setProperty("blade.env", profile); + props.setProperty("blade.name", appName); + props.setProperty("blade.is-local", String.valueOf(isLocalDev())); + props.setProperty("blade.dev-mode", profile.equals(AppConstant.PROD_CODE) ? "false" : "true"); + props.setProperty("blade.service.version", AppConstant.APPLICATION_VERSION); + // 加载自定义组件 + ServiceLoader loader = ServiceLoader.load(LauncherService.class); + loader.forEach(launcherService -> launcherService.launcher(builder, appName, profile)); + return builder; + } + + /** + * 判断是否为本地开发环境 + * + * @return boolean + */ + private static boolean isLocalDev() { + String osName = System.getProperty("os.name"); + return StringUtils.hasText(osName) && !(AppConstant.OS_NAME_LINUX.equals(osName.toUpperCase())); + } + +} diff --git a/blade-core-launch/src/main/java/org/springblade/core/launch/BladeLineRunner.java b/blade-core-launch/src/main/java/org/springblade/core/launch/BladeLineRunner.java new file mode 100644 index 0000000..a0b9fda --- /dev/null +++ b/blade-core-launch/src/main/java/org/springblade/core/launch/BladeLineRunner.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.launch; + +import org.springframework.boot.CommandLineRunner; +import org.springframework.stereotype.Component; + +/** + * 系统启动完毕后执行 + */ +@Component +public class BladeLineRunner implements CommandLineRunner { + + @Override + public void run(String... args) { + + } + +} diff --git a/blade-core-launch/src/main/java/org/springblade/core/launch/StartEventListener.java b/blade-core-launch/src/main/java/org/springblade/core/launch/StartEventListener.java new file mode 100644 index 0000000..939f5fd --- /dev/null +++ b/blade-core-launch/src/main/java/org/springblade/core/launch/StartEventListener.java @@ -0,0 +1,29 @@ +package org.springblade.core.launch; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.web.context.WebServerInitializedEvent; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.event.EventListener; +import org.springframework.core.annotation.Order; +import org.springframework.core.env.Environment; +import org.springframework.scheduling.annotation.Async; +import org.springframework.util.StringUtils; + +/** + * 项目启动事件通知 + */ +@Slf4j +@Configuration +public class StartEventListener { + + @Async + @Order + @EventListener(WebServerInitializedEvent.class) + public void afterStart(WebServerInitializedEvent event) { + Environment environment = event.getApplicationContext().getEnvironment(); + String appName = environment.getProperty("spring.application.name").toUpperCase(); + int localPort = event.getWebServer().getPort(); + String profile = StringUtils.arrayToCommaDelimitedString(environment.getActiveProfiles()); + log.info("---[{}]---启动完成,当前使用的端口:[{}],环境变量:[{}]---", appName, localPort, profile); + } +} diff --git a/blade-core-launch/src/main/java/org/springblade/core/launch/config/BladeConsulServiceRegistryConfiguration.java b/blade-core-launch/src/main/java/org/springblade/core/launch/config/BladeConsulServiceRegistryConfiguration.java new file mode 100644 index 0000000..7b86567 --- /dev/null +++ b/blade-core-launch/src/main/java/org/springblade/core/launch/config/BladeConsulServiceRegistryConfiguration.java @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.launch.config; + +import com.ecwid.consul.v1.ConsulClient; +import org.springblade.core.launch.consul.BladeConsulServiceRegistry; +import org.springblade.core.launch.server.ServerInfo; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.cloud.consul.ConditionalOnConsulEnabled; +import org.springframework.cloud.consul.discovery.ConsulDiscoveryProperties; +import org.springframework.cloud.consul.discovery.HeartbeatProperties; +import org.springframework.cloud.consul.discovery.TtlScheduler; +import org.springframework.cloud.consul.serviceregistry.ConsulServiceRegistry; +import org.springframework.cloud.consul.serviceregistry.ConsulServiceRegistryAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Consul自定义注册规则 + */ +@Configuration +@ConditionalOnConsulEnabled +@AutoConfigureBefore(ConsulServiceRegistryAutoConfiguration.class) +public class BladeConsulServiceRegistryConfiguration { + + @Autowired(required = false) + private TtlScheduler ttlScheduler; + + @Autowired + private ServerInfo serverInfo; + + @Bean + public ConsulServiceRegistry consulServiceRegistry(ConsulClient consulClient, ConsulDiscoveryProperties properties, + HeartbeatProperties heartbeatProperties) { + return new BladeConsulServiceRegistry(consulClient, properties, ttlScheduler, heartbeatProperties, serverInfo); + } + +} diff --git a/blade-core-launch/src/main/java/org/springblade/core/launch/config/BladeLaunchConfiguration.java b/blade-core-launch/src/main/java/org/springblade/core/launch/config/BladeLaunchConfiguration.java new file mode 100644 index 0000000..c019f0c --- /dev/null +++ b/blade-core-launch/src/main/java/org/springblade/core/launch/config/BladeLaunchConfiguration.java @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.launch.config; + +import lombok.AllArgsConstructor; +import org.springblade.core.launch.props.BladeProperties; +import org.springblade.core.launch.server.ServerInfo; +import org.springframework.boot.autoconfigure.web.ServerProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cloud.commons.util.InetUtils; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; + + +@Configuration +@AllArgsConstructor +@Order(Ordered.HIGHEST_PRECEDENCE) +@EnableConfigurationProperties({ + BladeProperties.class +}) +public class BladeLaunchConfiguration { + + private ServerProperties serverProperties; + private InetUtils inetUtils; + + + /** + * 服务器信息 + */ + @Bean + public ServerInfo serverInfo() { + return new ServerInfo(serverProperties, inetUtils); + } + +} diff --git a/blade-core-launch/src/main/java/org/springblade/core/launch/constant/AppConstant.java b/blade-core-launch/src/main/java/org/springblade/core/launch/constant/AppConstant.java new file mode 100644 index 0000000..c4b2b86 --- /dev/null +++ b/blade-core-launch/src/main/java/org/springblade/core/launch/constant/AppConstant.java @@ -0,0 +1,146 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.launch.constant; + +/** + * 系统常量 + */ +public interface AppConstant { + + /** + * 应用版本 + */ + String APPLICATION_VERSION = "1.0.0"; + + /** + * consul dev 地址 + */ + String CONSUL_DEV_HOST = "http://localhost"; + + /** + * consul prod 地址 + */ + String CONSUL_PROD_HOST = "http://192.168.186.129"; + + /** + * consul端口 + */ + String CONSUL_PORT = "8500"; + + /** + * consul端口 + */ + String CONSUL_CONFIG_FORMAT = "yaml"; + + /** + * consul端口 + */ + String CONSUL_WATCH_DELAY = "1000"; + + /** + * consul端口 + */ + String CONSUL_WATCH_ENABLED = "true"; + + /** + * 基础包 + */ + String BASE_PACKAGES = "org.springblade"; + + /** + * zookeeper id + */ + String ZOOKEEPER_ID = "zk"; + + /** + * zookeeper connect string + */ + String ZOOKEEPER_CONNECT_STRING = "127.0.0.1:2181"; + + /** + * zookeeper address + */ + String ZOOKEEPER_ADDRESS = "zookeeper://" + ZOOKEEPER_CONNECT_STRING; + + /** + * zookeeper root + */ + String ZOOKEEPER_ROOT = "/blade-services"; + + /** + * 应用名前缀 + */ + String APPLICATION_NAME_FREFIX = "blade-"; + /** + * 网关模块名称 + */ + String APPLICATION_GATEWAY_NAME = APPLICATION_NAME_FREFIX + "gateway"; + /** + * 授权模块名称 + */ + String APPLICATION_AUTH_NAME = APPLICATION_NAME_FREFIX + "auth"; + /** + * 监控模块名称 + */ + String APPLICATION_ADMIN_NAME = APPLICATION_NAME_FREFIX + "admin"; + /** + * 配置中心模块名称 + */ + String APPLICATION_CONFIG_NAME = APPLICATION_NAME_FREFIX + "config-server"; + /** + * TX模块名称 + */ + String APPLICATION_TX_MANAGER = "tx-manager"; + /** + * 首页模块名称 + */ + String APPLICATION_DESK_NAME = APPLICATION_NAME_FREFIX + "desk"; + /** + * 系统模块名称 + */ + String APPLICATION_SYSTEM_NAME = APPLICATION_NAME_FREFIX + "system"; + /** + * 用户模块名称 + */ + String APPLICATION_USER_NAME = APPLICATION_NAME_FREFIX + "user"; + /** + * 日志模块名称 + */ + String APPLICATION_LOG_NAME = APPLICATION_NAME_FREFIX + "log"; + /** + * 测试模块名称 + */ + String APPLICATION_TEST_NAME = APPLICATION_NAME_FREFIX + "test"; + + /** + * 开发环境 + */ + String DEV_CDOE = "dev"; + /** + * 生产环境 + */ + String PROD_CODE = "prod"; + /** + * 测试环境 + */ + String TEST_CODE = "test"; + + /** + * 代码部署于 linux 上,工作默认为 mac 和 Windows + */ + String OS_NAME_LINUX = "LINUX"; + +} diff --git a/blade-core-launch/src/main/java/org/springblade/core/launch/consul/BladeConsulServiceRegistry.java b/blade-core-launch/src/main/java/org/springblade/core/launch/consul/BladeConsulServiceRegistry.java new file mode 100644 index 0000000..b5d0492 --- /dev/null +++ b/blade-core-launch/src/main/java/org/springblade/core/launch/consul/BladeConsulServiceRegistry.java @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.launch.consul; + +import com.ecwid.consul.v1.ConsulClient; +import org.springblade.core.launch.server.ServerInfo; +import org.springframework.cloud.consul.discovery.ConsulDiscoveryProperties; +import org.springframework.cloud.consul.discovery.HeartbeatProperties; +import org.springframework.cloud.consul.discovery.TtlScheduler; +import org.springframework.cloud.consul.serviceregistry.ConsulRegistration; +import org.springframework.cloud.consul.serviceregistry.ConsulServiceRegistry; + +/** + * Consul自定义注册规则 + */ +public class BladeConsulServiceRegistry extends ConsulServiceRegistry { + + private ServerInfo serverInfo; + + public BladeConsulServiceRegistry(ConsulClient client, ConsulDiscoveryProperties properties, TtlScheduler ttlScheduler, HeartbeatProperties heartbeatProperties, ServerInfo serverInfo) { + super(client, properties, ttlScheduler, heartbeatProperties); + this.serverInfo = serverInfo; + } + + @Override + public void register(ConsulRegistration reg) { + reg.getService().setId(reg.getService().getName() + "-" + serverInfo.getIP() + "-" + serverInfo.getPort()); + super.register(reg); + } + +} diff --git a/blade-core-launch/src/main/java/org/springblade/core/launch/consul/ConsulLauncherService.java b/blade-core-launch/src/main/java/org/springblade/core/launch/consul/ConsulLauncherService.java new file mode 100644 index 0000000..d0e7daf --- /dev/null +++ b/blade-core-launch/src/main/java/org/springblade/core/launch/consul/ConsulLauncherService.java @@ -0,0 +1,24 @@ +package org.springblade.core.launch.consul; + +import org.springblade.core.launch.constant.AppConstant; +import org.springblade.core.launch.service.LauncherService; +import org.springframework.boot.builder.SpringApplicationBuilder; + +import java.util.Properties; + +/** + * consul启动拓展 + */ +public class ConsulLauncherService implements LauncherService { + + @Override + public void launcher(SpringApplicationBuilder builder, String appName, String profile) { + Properties props = System.getProperties(); + props.setProperty("spring.cloud.consul.host", profile.equals(AppConstant.DEV_CDOE) ? AppConstant.CONSUL_DEV_HOST : AppConstant.CONSUL_PROD_HOST); + props.setProperty("spring.cloud.consul.port", AppConstant.CONSUL_PORT); + props.setProperty("spring.cloud.consul.config.format", AppConstant.CONSUL_CONFIG_FORMAT); + props.setProperty("spring.cloud.consul.watch.delay", AppConstant.CONSUL_WATCH_DELAY); + props.setProperty("spring.cloud.consul.watch.enabled", AppConstant.CONSUL_WATCH_ENABLED); + } + +} diff --git a/blade-core-launch/src/main/java/org/springblade/core/launch/props/BladeProperties.java b/blade-core-launch/src/main/java/org/springblade/core/launch/props/BladeProperties.java new file mode 100644 index 0000000..f176944 --- /dev/null +++ b/blade-core-launch/src/main/java/org/springblade/core/launch/props/BladeProperties.java @@ -0,0 +1,210 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.launch.props; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.lang.Nullable; + +import java.util.HashMap; +import java.util.Map; + +/** + * 配置文件 + */ +@ConfigurationProperties("blade") +public class BladeProperties { + + /** + * 开发环境 + */ + @Getter + @Setter + private String env; + + /** + * 服务名 + */ + @Getter + @Setter + private String name; + + /** + * 判断是否为 本地开发环境 + */ + @Getter + @Setter + private Boolean isLocal = Boolean.FALSE; + + /** + * 装载自定义配置blade.prop.xxx + */ + @Getter + private final Map prop = new HashMap<>(); + + /** + * 获取配置 + * + * @param key key + * @return value + */ + @Nullable + public String get(String key) { + return get(key, null); + } + + /** + * 获取配置 + * + * @param key key + * @param defaultValue 默认值 + * @return value + */ + @Nullable + public String get(String key, @Nullable String defaultValue) { + String value = prop.get(key); + if (value == null) { + return defaultValue; + } + return value; + } + + /** + * 获取配置 + * + * @param key key + * @return int value + */ + @Nullable + public Integer getInt(String key) { + return getInt(key, null); + } + + /** + * 获取配置 + * + * @param key key + * @param defaultValue 默认值 + * @return int value + */ + @Nullable + public Integer getInt(String key, @Nullable Integer defaultValue) { + String value = prop.get(key); + if (value != null) { + return Integer.valueOf(value.trim()); + } + return defaultValue; + } + + /** + * 获取配置 + * + * @param key key + * @return long value + */ + @Nullable + public Long getLong(String key) { + return getLong(key, null); + } + + /** + * 获取配置 + * + * @param key key + * @param defaultValue 默认值 + * @return long value + */ + @Nullable + public Long getLong(String key, @Nullable Long defaultValue) { + String value = prop.get(key); + if (value != null) { + return Long.valueOf(value.trim()); + } + return defaultValue; + } + + /** + * 获取配置 + * + * @param key key + * @return Boolean value + */ + @Nullable + public Boolean getBoolean(String key) { + return getBoolean(key, null); + } + + /** + * 获取配置 + * + * @param key key + * @param defaultValue 默认值 + * @return Boolean value + */ + @Nullable + public Boolean getBoolean(String key, @Nullable Boolean defaultValue) { + String value = prop.get(key); + if (value != null) { + value = value.toLowerCase().trim(); + if ("true".equals(value)) { + return Boolean.TRUE; + } else if ("false".equals(value)) { + return Boolean.FALSE; + } + throw new RuntimeException("The value can not parse to Boolean : " + value); + } + return defaultValue; + } + + /** + * 获取配置 + * + * @param key key + * @return double value + */ + @Nullable + public Double getDouble(String key) { + return getDouble(key, null); + } + + /** + * 获取配置 + * + * @param key key + * @param defaultValue 默认值 + * @return double value + */ + @Nullable + public Double getDouble(String key, @Nullable Double defaultValue) { + String value = prop.get(key); + if (value != null) { + return Double.parseDouble(value.trim()); + } + return defaultValue; + } + + /** + * 判断是否存在key + * + * @param key prop key + * @return boolean + */ + public boolean containsKey(String key) { + return prop.containsKey(key); + } + +} diff --git a/blade-core-launch/src/main/java/org/springblade/core/launch/server/ServerInfo.java b/blade-core-launch/src/main/java/org/springblade/core/launch/server/ServerInfo.java new file mode 100644 index 0000000..d936385 --- /dev/null +++ b/blade-core-launch/src/main/java/org/springblade/core/launch/server/ServerInfo.java @@ -0,0 +1,62 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.launch.server; + +import org.springframework.boot.autoconfigure.web.ServerProperties; +import org.springframework.cloud.commons.util.InetUtils; + +/** + * 服务器信息 + */ +public class ServerInfo { + + private ServerProperties serverProperties; + private InetUtils inetUtils; + private String hostName; + private String ip; + private Integer prot; + private String ipWithPort; + + public ServerInfo(ServerProperties serverProperties, InetUtils inetUtils) { + this.serverProperties = serverProperties; + this.inetUtils = inetUtils; + this.hostName = getHostInfo().getHostname(); + this.ip = getHostInfo().getIpAddress(); + this.prot = serverProperties.getPort(); + this.ipWithPort = String.format("%s:%d", ip, prot); + } + + public InetUtils.HostInfo getHostInfo() { + return inetUtils.findFirstNonLoopbackHostInfo(); + } + + public String getIP() { + return this.ip; + } + + public Integer getPort() { + return this.prot; + } + + public String getHostName() { + return this.hostName; + } + + public String getIPWithPort() { + return this.ipWithPort; + } + +} diff --git a/blade-core-launch/src/main/java/org/springblade/core/launch/service/LauncherService.java b/blade-core-launch/src/main/java/org/springblade/core/launch/service/LauncherService.java new file mode 100644 index 0000000..9b24b1e --- /dev/null +++ b/blade-core-launch/src/main/java/org/springblade/core/launch/service/LauncherService.java @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.launch.service; + +import org.springframework.boot.builder.SpringApplicationBuilder; + +/** + * launcher 扩展 用于一些组件发现 + */ +public interface LauncherService { + + /** + * 启动时 处理 SpringApplicationBuilder + * @param builder SpringApplicationBuilder + * @param appName SpringApplicationAppName + * @param profile SpringApplicationProfile + */ + void launcher(SpringApplicationBuilder builder, String appName, String profile); + +} diff --git a/blade-core-launch/src/main/resources/META-INF/services/org.springblade.core.launch.service.LauncherService b/blade-core-launch/src/main/resources/META-INF/services/org.springblade.core.launch.service.LauncherService new file mode 100644 index 0000000..adb5545 --- /dev/null +++ b/blade-core-launch/src/main/resources/META-INF/services/org.springblade.core.launch.service.LauncherService @@ -0,0 +1 @@ +org.springblade.core.launch.consul.ConsulLauncherService diff --git a/blade-core-launch/src/main/resources/META-INF/spring-configuration-metadata.json b/blade-core-launch/src/main/resources/META-INF/spring-configuration-metadata.json new file mode 100644 index 0000000..d30dadb --- /dev/null +++ b/blade-core-launch/src/main/resources/META-INF/spring-configuration-metadata.json @@ -0,0 +1,37 @@ +{ + "hints": [], + "groups": [ + { + "sourceType": "org.springblade.core.launch.props.BladeProperties", + "name": "blade", + "type": "org.springblade.core.launch.props.BladeProperties" + } + ], + "properties": [ + { + "sourceType": "org.springblade.core.launch.props.BladeProperties", + "name": "blade.env", + "description": "开发环境", + "type": "java.lang.String" + }, + { + "sourceType": "org.springblade.core.launch.props.BladeProperties", + "defaultValue": false, + "name": "blade.is-local", + "description": "判断是否为 本地开发环境", + "type": "java.lang.Boolean" + }, + { + "sourceType": "org.springblade.core.launch.props.BladeProperties", + "name": "blade.name", + "description": "服务名", + "type": "java.lang.String" + }, + { + "sourceType": "org.springblade.core.launch.props.BladeProperties", + "name": "blade.prop", + "description": "装载自定义配置blade.prop.xxx", + "type": "java.util.Map" + } + ] +} \ No newline at end of file diff --git a/blade-core-launch/src/main/resources/META-INF/spring-devtools.properties b/blade-core-launch/src/main/resources/META-INF/spring-devtools.properties new file mode 100644 index 0000000..83627c5 --- /dev/null +++ b/blade-core-launch/src/main/resources/META-INF/spring-devtools.properties @@ -0,0 +1 @@ +restart.include.blade-core-launch=/blade-core-launch[\\w-]+\.jar diff --git a/blade-core-launch/src/main/resources/META-INF/spring.factories b/blade-core-launch/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..a7c7070 --- /dev/null +++ b/blade-core-launch/src/main/resources/META-INF/spring.factories @@ -0,0 +1,4 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + org.springblade.core.launch.config.BladeLaunchConfiguration,\ + org.springblade.core.launch.StartEventListener,\ + org.springblade.core.launch.config.BladeConsulServiceRegistryConfiguration diff --git a/blade-core-log/pom.xml b/blade-core-log/pom.xml new file mode 100644 index 0000000..a8f6bb9 --- /dev/null +++ b/blade-core-log/pom.xml @@ -0,0 +1,43 @@ + + + + blade-tool + org.springblade + 1.0.0-RC1 + + + 4.0.0 + + blade-core-log + ${project.artifactId} + ${blade.tool.version} + jar + + + + + org.springblade + blade-core-tool + ${blade.tool.version} + + + org.springblade + blade-core-secure + ${blade.tool.version} + + + + com.baomidou + mybatis-plus + ${mybatis.plus.version} + + + + org.springframework.cloud + spring-cloud-starter-openfeign + + + + diff --git a/blade-core-log/src/main/java/org/springblade/core/log/annotation/ApiLog.java b/blade-core-log/src/main/java/org/springblade/core/log/annotation/ApiLog.java new file mode 100644 index 0000000..b9563d4 --- /dev/null +++ b/blade-core-log/src/main/java/org/springblade/core/log/annotation/ApiLog.java @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springblade.core.log.annotation; + +import java.lang.annotation.*; + +/** + * 操作日志注解 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface ApiLog { + + /** + * 日志描述 + * + * @return {String} + */ + String value() default "日志记录"; +} diff --git a/blade-core-log/src/main/java/org/springblade/core/log/aspect/ApiLogAspect.java b/blade-core-log/src/main/java/org/springblade/core/log/aspect/ApiLogAspect.java new file mode 100644 index 0000000..b55b9e1 --- /dev/null +++ b/blade-core-log/src/main/java/org/springblade/core/log/aspect/ApiLogAspect.java @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springblade.core.log.aspect; + +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.springblade.core.log.annotation.ApiLog; +import org.springblade.core.log.publisher.ApiLogPublisher; + +/** + * 操作日志使用spring event异步入库 + */ +@Slf4j +@Aspect +public class ApiLogAspect { + + @Around("@annotation(apiLog)") + public Object around(ProceedingJoinPoint point, ApiLog apiLog) throws Throwable { + //获取类名 + String className = point.getTarget().getClass().getName(); + //获取方法 + String methodName = point.getSignature().getName(); + // 发送异步日志事件 + long beginTime = System.currentTimeMillis(); + //执行方法 + Object result = point.proceed(); + //执行时长(毫秒) + long time = System.currentTimeMillis() - beginTime; + //记录日志 + ApiLogPublisher.publishEvent(methodName, className, apiLog, time); + return result; + } + +} diff --git a/blade-core-log/src/main/java/org/springblade/core/log/config/BladeErrorMvcAutoConfiguration.java b/blade-core-log/src/main/java/org/springblade/core/log/config/BladeErrorMvcAutoConfiguration.java new file mode 100644 index 0000000..918b1bd --- /dev/null +++ b/blade-core-log/src/main/java/org/springblade/core/log/config/BladeErrorMvcAutoConfiguration.java @@ -0,0 +1,63 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.log.config; + + +import lombok.AllArgsConstructor; +import org.springblade.core.log.error.BladeErrorAttributes; +import org.springblade.core.log.error.BladeErrorController; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.boot.autoconfigure.condition.SearchStrategy; +import org.springframework.boot.autoconfigure.web.ServerProperties; +import org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController; +import org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration; +import org.springframework.boot.web.servlet.error.DefaultErrorAttributes; +import org.springframework.boot.web.servlet.error.ErrorAttributes; +import org.springframework.boot.web.servlet.error.ErrorController; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.DispatcherServlet; + +import javax.servlet.Servlet; + +/** + * 统一异常处理 + */ +@Configuration +@AllArgsConstructor +@ConditionalOnWebApplication +@AutoConfigureBefore(ErrorMvcAutoConfiguration.class) +@ConditionalOnClass({ Servlet.class, DispatcherServlet.class }) +public class BladeErrorMvcAutoConfiguration { + + private final ServerProperties serverProperties; + + @Bean + @ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT) + public DefaultErrorAttributes errorAttributes() { + return new BladeErrorAttributes(); + } + + @Bean + @ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT) + public BasicErrorController basicErrorController(ErrorAttributes errorAttributes) { + return new BladeErrorController(errorAttributes, serverProperties.getError()); + } + +} diff --git a/blade-core-log/src/main/java/org/springblade/core/log/config/BladeLogToolAutoConfiguration.java b/blade-core-log/src/main/java/org/springblade/core/log/config/BladeLogToolAutoConfiguration.java new file mode 100644 index 0000000..095ce5a --- /dev/null +++ b/blade-core-log/src/main/java/org/springblade/core/log/config/BladeLogToolAutoConfiguration.java @@ -0,0 +1,69 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springblade.core.log.config; + +import lombok.AllArgsConstructor; +import org.springblade.core.log.aspect.ApiLogAspect; +import org.springblade.core.log.event.ApiLogListener; +import org.springblade.core.log.event.BladeLogListener; +import org.springblade.core.log.event.ErrorLogListener; +import org.springblade.core.log.logger.BladeLogger; +import org.springblade.core.launch.props.BladeProperties; +import org.springblade.core.launch.server.ServerInfo; +import org.springblade.core.log.feign.ILogClient; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * 日志工具自动配置 + */ +@Configuration +@AllArgsConstructor +@ConditionalOnWebApplication +public class BladeLogToolAutoConfiguration { + + private final ILogClient logService; + private final ServerInfo serverInfo; + private final BladeProperties bladeProperties; + + @Bean + public ApiLogAspect apiLogAspect() { + return new ApiLogAspect(); + } + + @Bean + public BladeLogger bladeLogger() { + return new BladeLogger(); + } + + @Bean + public ApiLogListener apiLogListener() { + return new ApiLogListener(logService, serverInfo, bladeProperties); + } + + @Bean + public ErrorLogListener errorEventListener() { + return new ErrorLogListener(logService, serverInfo, bladeProperties); + } + + @Bean + public BladeLogListener bladeEventListener() { + return new BladeLogListener(logService, serverInfo, bladeProperties); + } + +} diff --git a/blade-core-log/src/main/java/org/springblade/core/log/constant/EventConstant.java b/blade-core-log/src/main/java/org/springblade/core/log/constant/EventConstant.java new file mode 100644 index 0000000..9d4fc37 --- /dev/null +++ b/blade-core-log/src/main/java/org/springblade/core/log/constant/EventConstant.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.log.constant; + +/** + * 事件常量 + */ +public interface EventConstant { + + /** + * log + */ + String EVENT_LOG = "log"; + /** + * request + */ + String EVENT_REQUEST = "request"; + +} diff --git a/blade-core-log/src/main/java/org/springblade/core/log/error/BladeErrorAttributes.java b/blade-core-log/src/main/java/org/springblade/core/log/error/BladeErrorAttributes.java new file mode 100644 index 0000000..cb1dcb4 --- /dev/null +++ b/blade-core-log/src/main/java/org/springblade/core/log/error/BladeErrorAttributes.java @@ -0,0 +1,59 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.log.error; + +import lombok.extern.slf4j.Slf4j; +import org.springblade.core.log.publisher.ErrorLogPublisher; +import org.springblade.core.tool.api.R; +import org.springblade.core.tool.api.ResultCode; +import org.springblade.core.tool.utils.BeanUtil; +import org.springframework.boot.web.servlet.error.DefaultErrorAttributes; +import org.springframework.lang.Nullable; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.WebRequest; + +import java.util.Map; + +/** + * 全局异常处理 + */ +@Slf4j +public class BladeErrorAttributes extends DefaultErrorAttributes { + + @Override + public Map getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) { + String requestUri = this.getAttr(webRequest, "javax.servlet.error.request_uri"); + Integer status = this.getAttr(webRequest, "javax.servlet.error.status_code"); + Throwable error = getError(webRequest); + R result; + if (error == null) { + log.error("URL:{} error status:{}", requestUri, status); + result = R.failure(ResultCode.FAILURE, "系统未知异常[HttpStatus]:" + status); + } else { + log.error(String.format("URL:%s error status:%d", requestUri, status), error); + result = R.failure(status, error.getMessage()); + } + //发送服务异常事件 + ErrorLogPublisher.publishEvent(error, requestUri); + return BeanUtil.toMap(result); + } + + @Nullable + private T getAttr(WebRequest webRequest, String name) { + return (T) webRequest.getAttribute(name, RequestAttributes.SCOPE_REQUEST); + } + +} diff --git a/blade-core-log/src/main/java/org/springblade/core/log/error/BladeErrorController.java b/blade-core-log/src/main/java/org/springblade/core/log/error/BladeErrorController.java new file mode 100644 index 0000000..d3b4c74 --- /dev/null +++ b/blade-core-log/src/main/java/org/springblade/core/log/error/BladeErrorController.java @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.log.error; + +import org.springblade.core.tool.jackson.JsonUtil; +import org.springframework.boot.autoconfigure.web.ErrorProperties; +import org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController; +import org.springframework.boot.web.servlet.error.ErrorAttributes; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.view.json.MappingJackson2JsonView; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.Map; + +/** + * 更改html请求异常为ajax + */ +public class BladeErrorController extends BasicErrorController { + + public BladeErrorController(ErrorAttributes errorAttributes, ErrorProperties errorProperties) { + super(errorAttributes, errorProperties); + } + + @Override + public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) { + Map body = getErrorAttributes(request, isIncludeStackTrace(request, MediaType.ALL)); + HttpStatus status = getStatus(request); + response.setStatus(status.value()); + MappingJackson2JsonView view = new MappingJackson2JsonView(); + view.setObjectMapper(JsonUtil.getInstance()); + view.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE); + return new ModelAndView(view, body); + } + +} diff --git a/blade-core-log/src/main/java/org/springblade/core/log/error/BladeRestExceptionTranslator.java b/blade-core-log/src/main/java/org/springblade/core/log/error/BladeRestExceptionTranslator.java new file mode 100644 index 0000000..24dca6e --- /dev/null +++ b/blade-core-log/src/main/java/org/springblade/core/log/error/BladeRestExceptionTranslator.java @@ -0,0 +1,160 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.log.error; + +import lombok.extern.slf4j.Slf4j; +import org.hibernate.validator.internal.engine.path.PathImpl; +import org.springblade.core.log.exception.ServiceException; +import org.springblade.core.log.publisher.ErrorLogPublisher; +import org.springblade.core.secure.exception.SecureException; +import org.springblade.core.tool.api.R; +import org.springblade.core.tool.api.ResultCode; +import org.springblade.core.tool.utils.Func; +import org.springblade.core.tool.utils.URLUtil; +import org.springblade.core.tool.utils.WebUtil; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpStatus; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.validation.BindException; +import org.springframework.validation.BindingResult; +import org.springframework.validation.FieldError; +import org.springframework.web.HttpMediaTypeNotSupportedException; +import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingServletRequestParameterException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; +import org.springframework.web.servlet.DispatcherServlet; +import org.springframework.web.servlet.NoHandlerFoundException; + +import javax.servlet.Servlet; +import javax.validation.ConstraintViolation; +import javax.validation.ConstraintViolationException; +import java.util.Set; + +/** + * 全局异常处理,处理可预见的异常 + */ +@Slf4j +@Configuration +@ConditionalOnClass({ Servlet.class, DispatcherServlet.class }) +@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) +@RestControllerAdvice +public class BladeRestExceptionTranslator { + + @ExceptionHandler(MissingServletRequestParameterException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public R handleError(MissingServletRequestParameterException e) { + log.warn("缺少请求参数", e.getMessage()); + String message = String.format("缺少必要的请求参数: %s", e.getParameterName()); + return R.failure(ResultCode.PARAM_MISS, message); + } + + @ExceptionHandler(MethodArgumentTypeMismatchException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public R handleError(MethodArgumentTypeMismatchException e) { + log.warn("请求参数格式错误", e.getMessage()); + String message = String.format("请求参数格式错误: %s", e.getName()); + return R.failure(ResultCode.PARAM_TYPE_ERROR, message); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public R handleError(MethodArgumentNotValidException e) { + log.warn("参数验证失败", e.getMessage()); + return handleError(e.getBindingResult()); + } + + @ExceptionHandler(BindException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public R handleError(BindException e) { + log.warn("参数绑定失败", e.getMessage()); + return handleError(e.getBindingResult()); + } + + private R handleError(BindingResult result) { + FieldError error = result.getFieldError(); + String message = String.format("%s:%s", error.getField(), error.getDefaultMessage()); + return R.failure(ResultCode.PARAM_BIND_ERROR, message); + } + + @ExceptionHandler(ConstraintViolationException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public R handleError(ConstraintViolationException e) { + log.warn("参数验证失败", e.getMessage()); + Set> violations = e.getConstraintViolations(); + ConstraintViolation violation = violations.iterator().next(); + String path = ((PathImpl) violation.getPropertyPath()).getLeafNode().getName(); + String message = String.format("%s:%s", path, violation.getMessage()); + return R.failure(ResultCode.PARAM_VALID_ERROR, message); + } + + @ExceptionHandler(NoHandlerFoundException.class) + @ResponseStatus(HttpStatus.NOT_FOUND) + public R handleError(NoHandlerFoundException e) { + log.error("404没找到请求:{}", e.getMessage()); + return R.failure(ResultCode.NOT_FOUND, e.getMessage()); + } + + @ExceptionHandler(HttpMessageNotReadableException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public R handleError(HttpMessageNotReadableException e) { + log.error("消息不能读取:{}", e.getMessage()); + return R.failure(ResultCode.MSG_NOT_READABLE, e.getMessage()); + } + + @ExceptionHandler(HttpRequestMethodNotSupportedException.class) + @ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED) + public R handleError(HttpRequestMethodNotSupportedException e) { + log.error("不支持当前请求方法:{}", e.getMessage()); + return R.failure(ResultCode.METHOD_NOT_SUPPORTED, e.getMessage()); + } + + @ExceptionHandler(HttpMediaTypeNotSupportedException.class) + @ResponseStatus(HttpStatus.UNSUPPORTED_MEDIA_TYPE) + public R handleError(HttpMediaTypeNotSupportedException e) { + log.error("不支持当前媒体类型:{}", e.getMessage()); + return R.failure(ResultCode.MEDIA_TYPE_NOT_SUPPORTED, e.getMessage()); + } + + @ExceptionHandler(ServiceException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public R handleError(ServiceException e) { + log.error("业务异常", e); + return R.failure(e.getResultCode(), e.getMessage()); + } + + @ExceptionHandler(SecureException.class) + @ResponseStatus(HttpStatus.UNAUTHORIZED) + public R handleError(SecureException e) { + log.error("认证异常", e); + return R.failure(e.getResultCode(), e.getMessage()); + } + + @ExceptionHandler(Throwable.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public R handleError(Throwable e) { + log.error("服务器异常", e); + //发送服务异常事件 + ErrorLogPublisher.publishEvent(e, URLUtil.getPath(WebUtil.getRequest().getRequestURI())); + return R.failure(ResultCode.INTERNAL_SERVER_ERROR, (Func.isEmpty(e.getMessage()) ? ResultCode.INTERNAL_SERVER_ERROR.getMessage() : e.getMessage())); + } + +} diff --git a/blade-core-log/src/main/java/org/springblade/core/log/event/ApiLogEvent.java b/blade-core-log/src/main/java/org/springblade/core/log/event/ApiLogEvent.java new file mode 100644 index 0000000..1254bd4 --- /dev/null +++ b/blade-core-log/src/main/java/org/springblade/core/log/event/ApiLogEvent.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springblade.core.log.event; + +import org.springframework.context.ApplicationEvent; + +import java.util.Map; + +/** + * 系统日志事件 + */ +public class ApiLogEvent extends ApplicationEvent { + + public ApiLogEvent(Map source) { + super(source); + } + +} diff --git a/blade-core-log/src/main/java/org/springblade/core/log/event/ApiLogListener.java b/blade-core-log/src/main/java/org/springblade/core/log/event/ApiLogListener.java new file mode 100644 index 0000000..46bc582 --- /dev/null +++ b/blade-core-log/src/main/java/org/springblade/core/log/event/ApiLogListener.java @@ -0,0 +1,73 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springblade.core.log.event; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springblade.core.log.constant.EventConstant; +import org.springblade.core.launch.props.BladeProperties; +import org.springblade.core.secure.utils.SecureUtil; +import org.springblade.core.launch.server.ServerInfo; +import org.springblade.core.tool.utils.URLUtil; +import org.springblade.core.tool.utils.WebUtil; +import org.springblade.core.log.feign.ILogClient; +import org.springblade.core.log.model.LogApi; +import org.springframework.context.event.EventListener; +import org.springframework.core.annotation.Order; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; + +import javax.servlet.http.HttpServletRequest; +import java.time.LocalDateTime; +import java.util.Map; + + +/** + * 异步监听日志事件 + */ +@Slf4j +@Component +@AllArgsConstructor +public class ApiLogListener { + + private final ILogClient logService; + private final ServerInfo serverInfo; + private final BladeProperties bladeProperties; + + + @Async + @Order + @EventListener(ApiLogEvent.class) + public void saveApiLog(ApiLogEvent event) { + Map source = (Map) event.getSource(); + LogApi logApi = (LogApi) source.get(EventConstant.EVENT_LOG); + HttpServletRequest request = (HttpServletRequest) source.get(EventConstant.EVENT_REQUEST); + logApi.setServiceId(bladeProperties.getName()); + logApi.setServerHost(serverInfo.getHostName()); + logApi.setServerIp(serverInfo.getIPWithPort()); + logApi.setEnv(bladeProperties.getEnv()); + logApi.setRemoteIp(WebUtil.getIP(request)); + logApi.setUserAgent(request.getHeader(WebUtil.USER_AGENT_HEADER)); + logApi.setRequestUri(URLUtil.getPath(request.getRequestURI())); + logApi.setMethod(request.getMethod()); + logApi.setParams(WebUtil.getRequestParamString(request)); + logApi.setCreateBy(SecureUtil.getUser(request).getAccount()); + logApi.setCreateTime(LocalDateTime.now()); + logService.saveApiLog(logApi); + } + +} diff --git a/blade-core-log/src/main/java/org/springblade/core/log/event/BladeLogEvent.java b/blade-core-log/src/main/java/org/springblade/core/log/event/BladeLogEvent.java new file mode 100644 index 0000000..c8bc540 --- /dev/null +++ b/blade-core-log/src/main/java/org/springblade/core/log/event/BladeLogEvent.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springblade.core.log.event; + +import org.springframework.context.ApplicationEvent; + +import java.util.Map; + +/** + * 系统日志事件 + */ +public class BladeLogEvent extends ApplicationEvent { + + public BladeLogEvent(Map source) { + super(source); + } + +} diff --git a/blade-core-log/src/main/java/org/springblade/core/log/event/BladeLogListener.java b/blade-core-log/src/main/java/org/springblade/core/log/event/BladeLogListener.java new file mode 100644 index 0000000..dd565a7 --- /dev/null +++ b/blade-core-log/src/main/java/org/springblade/core/log/event/BladeLogListener.java @@ -0,0 +1,70 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.log.event; + + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springblade.core.launch.props.BladeProperties; +import org.springblade.core.launch.server.ServerInfo; +import org.springblade.core.log.constant.EventConstant; +import org.springblade.core.log.feign.ILogClient; +import org.springblade.core.log.model.LogBlade; +import org.springblade.core.secure.utils.SecureUtil; +import org.springblade.core.tool.utils.URLUtil; +import org.springblade.core.tool.utils.WebUtil; +import org.springframework.context.event.EventListener; +import org.springframework.core.annotation.Order; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; + +import javax.servlet.http.HttpServletRequest; +import java.time.LocalDateTime; +import java.util.Map; + +/** + * 异步监听日志事件 + */ +@Slf4j +@Component +@AllArgsConstructor +public class BladeLogListener { + + private final ILogClient logService; + private final ServerInfo serverInfo; + private final BladeProperties bladeProperties; + + @Async + @Order + @EventListener(BladeLogEvent.class) + public void saveBladeLog(BladeLogEvent event) { + Map source = (Map) event.getSource(); + LogBlade logBlade = (LogBlade) source.get(EventConstant.EVENT_LOG); + HttpServletRequest request = (HttpServletRequest) source.get(EventConstant.EVENT_REQUEST); + logBlade.setRequestUri(URLUtil.getPath(request.getRequestURI())); + logBlade.setUserAgent(request.getHeader(WebUtil.USER_AGENT_HEADER)); + logBlade.setMethod(request.getMethod()); + logBlade.setParams(WebUtil.getRequestParamString(request)); + logBlade.setServiceId(bladeProperties.getName()); + logBlade.setServerHost(serverInfo.getHostName()); + logBlade.setServerIp(serverInfo.getIPWithPort()); + logBlade.setEnv(bladeProperties.getEnv()); + logBlade.setCreateBy(SecureUtil.getUser(request).getAccount()); + logBlade.setCreateTime(LocalDateTime.now()); + logService.saveBladeLog(logBlade); + } + +} diff --git a/blade-core-log/src/main/java/org/springblade/core/log/event/ErrorLogEvent.java b/blade-core-log/src/main/java/org/springblade/core/log/event/ErrorLogEvent.java new file mode 100644 index 0000000..9c4c7db --- /dev/null +++ b/blade-core-log/src/main/java/org/springblade/core/log/event/ErrorLogEvent.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.log.event; + + +import org.springframework.context.ApplicationEvent; + +import java.util.Map; + +/** + * 错误日志事件 + */ +public class ErrorLogEvent extends ApplicationEvent { + + public ErrorLogEvent(Map source) { + super(source); + } + +} diff --git a/blade-core-log/src/main/java/org/springblade/core/log/event/ErrorLogListener.java b/blade-core-log/src/main/java/org/springblade/core/log/event/ErrorLogListener.java new file mode 100644 index 0000000..9b24aad --- /dev/null +++ b/blade-core-log/src/main/java/org/springblade/core/log/event/ErrorLogListener.java @@ -0,0 +1,68 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.log.event; + + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springblade.core.log.constant.EventConstant; +import org.springblade.core.launch.props.BladeProperties; +import org.springblade.core.secure.utils.SecureUtil; +import org.springblade.core.launch.server.ServerInfo; +import org.springblade.core.tool.utils.WebUtil; +import org.springblade.core.log.feign.ILogClient; +import org.springblade.core.log.model.LogError; +import org.springframework.context.event.EventListener; +import org.springframework.core.annotation.Order; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; + +import javax.servlet.http.HttpServletRequest; +import java.time.LocalDateTime; +import java.util.Map; + +/** + * 异步监听错误日志事件 + */ +@Slf4j +@Component +@AllArgsConstructor +public class ErrorLogListener { + + private final ILogClient logService; + private final ServerInfo serverInfo; + private final BladeProperties bladeProperties; + + @Async + @Order + @EventListener(ErrorLogEvent.class) + public void saveErrorLog(ErrorLogEvent event) { + Map source = (Map) event.getSource(); + LogError logError = (LogError) source.get(EventConstant.EVENT_LOG); + HttpServletRequest request = (HttpServletRequest) source.get(EventConstant.EVENT_REQUEST); + logError.setUserAgent(request.getHeader(WebUtil.USER_AGENT_HEADER)); + logError.setMethod(request.getMethod()); + logError.setParams(WebUtil.getRequestParamString(request)); + logError.setServiceId(bladeProperties.getName()); + logError.setServerHost(serverInfo.getHostName()); + logError.setServerIp(serverInfo.getIPWithPort()); + logError.setEnv(bladeProperties.getEnv()); + logError.setCreateBy(SecureUtil.getUser(request).getAccount()); + logError.setCreateTime(LocalDateTime.now()); + logService.saveErrorLog(logError); + } + +} diff --git a/blade-core-log/src/main/java/org/springblade/core/log/exception/ServiceException.java b/blade-core-log/src/main/java/org/springblade/core/log/exception/ServiceException.java new file mode 100644 index 0000000..74d5008 --- /dev/null +++ b/blade-core-log/src/main/java/org/springblade/core/log/exception/ServiceException.java @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.log.exception; + +import lombok.Getter; +import org.springblade.core.tool.api.IResultCode; +import org.springblade.core.tool.api.ResultCode; + + +/** + * 业务异常 + */ +public class ServiceException extends RuntimeException { + private static final long serialVersionUID = 2359767895161832954L; + + @Getter + private final IResultCode resultCode; + + public ServiceException(String message) { + super(message); + this.resultCode = ResultCode.INTERNAL_SERVER_ERROR; + } + + public ServiceException(IResultCode resultCode) { + super(resultCode.getMessage()); + this.resultCode = resultCode; + } + + public ServiceException(IResultCode resultCode, Throwable cause) { + super(cause); + this.resultCode = resultCode; + } + + /** + * 提高性能 + * @return Throwable + */ + @Override + public Throwable fillInStackTrace() { + return this; + } + + public Throwable doFillInStackTrace() { + return super.fillInStackTrace(); + } + +} diff --git a/blade-core-log/src/main/java/org/springblade/core/log/feign/ILogClient.java b/blade-core-log/src/main/java/org/springblade/core/log/feign/ILogClient.java new file mode 100644 index 0000000..59f4b8d --- /dev/null +++ b/blade-core-log/src/main/java/org/springblade/core/log/feign/ILogClient.java @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.log.feign; + +import org.springblade.core.launch.constant.AppConstant; +import org.springblade.core.log.model.LogApi; +import org.springblade.core.log.model.LogBlade; +import org.springblade.core.log.model.LogError; +import org.springblade.core.tool.api.R; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; + +/** + * Feign接口类 + */ +@FeignClient( + value = AppConstant.APPLICATION_LOG_NAME +) +public interface ILogClient { + + String API_PREFIX = "/log"; + + /** + * 保存错误日志 + * + * @param log + * @return + */ + @PostMapping(API_PREFIX + "/saveBladeLog") + R saveBladeLog(@RequestBody LogBlade log); + + /** + * 保存操作日志 + * + * @param log + * @return + */ + @PostMapping(API_PREFIX + "/saveApiLog") + R saveApiLog(@RequestBody LogApi log); + + /** + * 保存错误日志 + * + * @param log + * @return + */ + @PostMapping(API_PREFIX + "/saveErrorLog") + R saveErrorLog(@RequestBody LogError log); + +} diff --git a/blade-core-log/src/main/java/org/springblade/core/log/logger/BladeLogger.java b/blade-core-log/src/main/java/org/springblade/core/log/logger/BladeLogger.java new file mode 100644 index 0000000..778a6c2 --- /dev/null +++ b/blade-core-log/src/main/java/org/springblade/core/log/logger/BladeLogger.java @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.log.logger; + +import lombok.extern.slf4j.Slf4j; +import org.springblade.core.log.publisher.BladeLogPublisher; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Value; + +/** + * 日志工具类 + */ +@Slf4j +public class BladeLogger implements InitializingBean { + + @Value("${spring.application.name}") + private String serviceId; + + public void info(String id, String data) { + BladeLogPublisher.publishEvent("info", id, data); + } + + public void debug(String id, String data) { + BladeLogPublisher.publishEvent("debug", id, data); + } + + public void warn(String id, String data) { + BladeLogPublisher.publishEvent("warn", id, data); + } + + public void error(String id, String data) { + BladeLogPublisher.publishEvent("error", id, data); + } + + @Override + public void afterPropertiesSet() throws Exception { + log.info(serviceId + ": BladeLogger init success!"); + } + +} diff --git a/blade-core-log/src/main/java/org/springblade/core/log/model/LogApi.java b/blade-core-log/src/main/java/org/springblade/core/log/model/LogApi.java new file mode 100644 index 0000000..1fdd101 --- /dev/null +++ b/blade-core-log/src/main/java/org/springblade/core/log/model/LogApi.java @@ -0,0 +1,121 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.log.model; + + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; +import org.springblade.core.tool.date.DatePattern; +import org.springframework.format.annotation.DateTimeFormat; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 实体类 + * + * @author Blade + */ +@Data +@TableName("blade_log_api") +public class LogApi implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 主键id + */ + @TableId(value = "id", type = IdType.ID_WORKER) + private Long id; + + /** + * 日志类型 + */ + private String type; + /** + * 日志标题 + */ + private String title; + /** + * 服务ID + */ + private String serviceId; + /** + * 服务器 ip + */ + private String serverIp; + /** + * 服务器名 + */ + private String serverHost; + /** + * 环境 + */ + private String env; + /** + * 操作IP地址 + */ + private String remoteIp; + /** + * 用户代理 + */ + private String userAgent; + /** + * 请求URI + */ + private String requestUri; + /** + * 操作方式 + */ + private String method; + /** + * 方法类 + */ + private String methodClass; + /** + * 方法名 + */ + private String methodName; + /** + * 操作提交的数据 + */ + private String params; + /** + * 执行时间 + */ + private String time; + /** + * 异常信息 + */ + private String exception; + + /** + * 创建人 + */ + private String createBy; + + /** + * 创建时间 + */ + @DateTimeFormat(pattern = DatePattern.NORM_DATETIME_PATTERN) + @JsonFormat(pattern = DatePattern.NORM_DATETIME_PATTERN) + private LocalDateTime createTime; + + +} diff --git a/blade-core-log/src/main/java/org/springblade/core/log/model/LogBlade.java b/blade-core-log/src/main/java/org/springblade/core/log/model/LogBlade.java new file mode 100644 index 0000000..cd90e45 --- /dev/null +++ b/blade-core-log/src/main/java/org/springblade/core/log/model/LogBlade.java @@ -0,0 +1,103 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.log.model; + + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; +import org.springblade.core.tool.date.DatePattern; +import org.springframework.format.annotation.DateTimeFormat; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 实体类 + * + * @author Blade + * @since 2018-10-12 + */ +@Data +@TableName("blade_log") +public class LogBlade implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 主键id + */ + @TableId(value = "id", type = IdType.ID_WORKER) + private Long id; + /** + * 服务ID + */ + private String serviceId; + /** + * 服务器名 + */ + private String serverHost; + /** + * 服务器IP地址 + */ + private String serverIp; + /** + * 系统环境 + */ + private String env; + /** + * 日志级别 + */ + private String logLevel; + /** + * 日志业务id + */ + private String logId; + /** + * 日志数据 + */ + private String logData; + /** + * 操作方式 + */ + private String method; + /** + * 请求URI + */ + private String requestUri; + /** + * 用户代理 + */ + private String userAgent; + /** + * 操作提交的数据 + */ + private String params; + /** + * 创建者 + */ + private String createBy; + /** + * 创建时间 + */ + @DateTimeFormat(pattern = DatePattern.NORM_DATETIME_PATTERN) + @JsonFormat(pattern = DatePattern.NORM_DATETIME_PATTERN) + private LocalDateTime createTime; + + +} diff --git a/blade-core-log/src/main/java/org/springblade/core/log/model/LogError.java b/blade-core-log/src/main/java/org/springblade/core/log/model/LogError.java new file mode 100644 index 0000000..287798e --- /dev/null +++ b/blade-core-log/src/main/java/org/springblade/core/log/model/LogError.java @@ -0,0 +1,118 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.log.model; + + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; +import org.springblade.core.tool.date.DatePattern; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.lang.Nullable; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 服务 异常 + */ +@Data +@TableName("blade_log_error") +public class LogError implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 主键id + */ + @TableId(value = "id", type = IdType.ID_WORKER) + private Long id; + /** + * 应用名 + */ + private String serviceId; + /** + * 环境 + */ + private String env; + /** + * 服务器 ip + */ + private String serverIp; + /** + * 服务器名 + */ + private String serverHost; + /** + * 用户代理 + */ + private String userAgent; + /** + * 请求url + */ + @Nullable + private String requestUri; + /** + * 操作方式 + */ + private String method; + /** + * 堆栈信息 + */ + private String stackTrace; + /** + * 异常名 + */ + private String exceptionName; + /** + * 异常消息 + */ + private String message; + /** + * 类名 + */ + private String methodClass; + /** + * 文件名 + */ + private String fileName; + /** + * 方法名 + */ + private String methodName; + /** + * 操作提交的数据 + */ + private String params; + /** + * 代码行数 + */ + private Integer lineNumber; + + /** + * 创建人 + */ + private String createBy; + + /** + * 创建时间 + */ + @DateTimeFormat(pattern = DatePattern.NORM_DATETIME_PATTERN) + @JsonFormat(pattern = DatePattern.NORM_DATETIME_PATTERN) + private LocalDateTime createTime; +} diff --git a/blade-core-log/src/main/java/org/springblade/core/log/publisher/ApiLogPublisher.java b/blade-core-log/src/main/java/org/springblade/core/log/publisher/ApiLogPublisher.java new file mode 100644 index 0000000..2c4b690 --- /dev/null +++ b/blade-core-log/src/main/java/org/springblade/core/log/publisher/ApiLogPublisher.java @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springblade.core.log.publisher; + +import org.springblade.core.log.annotation.ApiLog; +import org.springblade.core.log.constant.EventConstant; +import org.springblade.core.log.event.ApiLogEvent; +import org.springblade.core.tool.constant.BladeConstant; +import org.springblade.core.tool.utils.SpringUtil; +import org.springblade.core.tool.utils.WebUtil; +import org.springblade.core.log.model.LogApi; + +import javax.servlet.http.HttpServletRequest; +import java.util.HashMap; +import java.util.Map; + +/** + * API日志信息事件发送 + */ +public class ApiLogPublisher { + + public static void publishEvent(String methodName, String methodClass, ApiLog apiLog, long time) { + HttpServletRequest request = WebUtil.getRequest(); + LogApi logApi = new LogApi(); + logApi.setType(BladeConstant.LOG_NORMAL_TYPE); + logApi.setTitle(apiLog.value()); + logApi.setTime(String.valueOf(time)); + logApi.setMethodClass(methodClass); + logApi.setMethodName(methodName); + Map event = new HashMap<>(); + event.put(EventConstant.EVENT_LOG, logApi); + event.put(EventConstant.EVENT_REQUEST, request); + SpringUtil.publishEvent(new ApiLogEvent(event)); + } + +} diff --git a/blade-core-log/src/main/java/org/springblade/core/log/publisher/BladeLogPublisher.java b/blade-core-log/src/main/java/org/springblade/core/log/publisher/BladeLogPublisher.java new file mode 100644 index 0000000..918800b --- /dev/null +++ b/blade-core-log/src/main/java/org/springblade/core/log/publisher/BladeLogPublisher.java @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springblade.core.log.publisher; + +import org.springblade.core.log.constant.EventConstant; +import org.springblade.core.log.event.BladeLogEvent; +import org.springblade.core.tool.utils.SpringUtil; +import org.springblade.core.tool.utils.WebUtil; +import org.springblade.core.log.model.LogBlade; + +import javax.servlet.http.HttpServletRequest; +import java.util.HashMap; +import java.util.Map; + +/** + * BLADE日志信息事件发送 + */ +public class BladeLogPublisher { + + public static void publishEvent(String level, String id, String data) { + HttpServletRequest request = WebUtil.getRequest(); + LogBlade logBlade = new LogBlade(); + logBlade.setLogLevel(level); + logBlade.setLogId(id); + logBlade.setLogData(data); + Map event = new HashMap<>(); + event.put(EventConstant.EVENT_LOG, logBlade); + event.put(EventConstant.EVENT_REQUEST, request); + SpringUtil.publishEvent(new BladeLogEvent(event)); + } + +} diff --git a/blade-core-log/src/main/java/org/springblade/core/log/publisher/ErrorLogPublisher.java b/blade-core-log/src/main/java/org/springblade/core/log/publisher/ErrorLogPublisher.java new file mode 100644 index 0000000..4748509 --- /dev/null +++ b/blade-core-log/src/main/java/org/springblade/core/log/publisher/ErrorLogPublisher.java @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springblade.core.log.publisher; + +import org.springblade.core.log.constant.EventConstant; +import org.springblade.core.log.event.ErrorLogEvent; +import org.springblade.core.tool.utils.*; +import org.springblade.core.log.model.LogError; + +import javax.servlet.http.HttpServletRequest; +import java.util.HashMap; +import java.util.Map; + +/** + * 异常信息事件发送 + */ +public class ErrorLogPublisher { + + public static void publishEvent(Throwable error, String requestUri) { + HttpServletRequest request = WebUtil.getRequest(); + LogError logError = new LogError(); + logError.setRequestUri(requestUri); + if (Func.isNotEmpty(error)) { + logError.setStackTrace(Exceptions.getStackTraceAsString(error)); + logError.setExceptionName(error.getClass().getName()); + logError.setMessage(error.getMessage()); + StackTraceElement[] elements = error.getStackTrace(); + if (Func.isNotEmpty(elements)) { + StackTraceElement element = elements[0]; + logError.setMethodName(element.getMethodName()); + logError.setMethodClass(element.getClassName()); + logError.setFileName(element.getFileName()); + logError.setLineNumber(element.getLineNumber()); + } + } + Map event = new HashMap<>(); + event.put(EventConstant.EVENT_LOG, logError); + event.put(EventConstant.EVENT_REQUEST, request); + SpringUtil.publishEvent(new ErrorLogEvent(event)); + } + +} diff --git a/blade-core-log/src/main/resources/META-INF/spring-devtools.properties b/blade-core-log/src/main/resources/META-INF/spring-devtools.properties new file mode 100644 index 0000000..dff854a --- /dev/null +++ b/blade-core-log/src/main/resources/META-INF/spring-devtools.properties @@ -0,0 +1 @@ +restart.include.blade-core-log=/blade-core-log[\\w-]+\.jar diff --git a/blade-core-log/src/main/resources/META-INF/spring.factories b/blade-core-log/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..3097701 --- /dev/null +++ b/blade-core-log/src/main/resources/META-INF/spring.factories @@ -0,0 +1,4 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + org.springblade.core.log.config.BladeLogToolAutoConfiguration,\ + org.springblade.core.log.config.BladeErrorMvcAutoConfiguration,\ + org.springblade.core.log.error.BladeRestExceptionTranslator diff --git a/blade-core-mybatis/pom.xml b/blade-core-mybatis/pom.xml new file mode 100644 index 0000000..7b518f6 --- /dev/null +++ b/blade-core-mybatis/pom.xml @@ -0,0 +1,33 @@ + + + + blade-tool + org.springblade + 1.0.0-RC1 + + + 4.0.0 + + blade-core-mybatis + ${project.artifactId} + ${blade.tool.version} + jar + + + + + com.baomidou + mybatis-plus + ${mybatis.plus.version} + + + + org.springblade + blade-core-secure + ${blade.tool.version} + + + + diff --git a/blade-core-mybatis/src/main/java/org/springblade/core/mp/BladeMetaObjectHandler.java b/blade-core-mybatis/src/main/java/org/springblade/core/mp/BladeMetaObjectHandler.java new file mode 100644 index 0000000..43cc93a --- /dev/null +++ b/blade-core-mybatis/src/main/java/org/springblade/core/mp/BladeMetaObjectHandler.java @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.mp; + +import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; +import lombok.extern.slf4j.Slf4j; +import org.apache.ibatis.reflection.MetaObject; + +/** + * mybatisplus自定义填充 + */ +@Slf4j +public class BladeMetaObjectHandler implements MetaObjectHandler { + + @Override + public void insertFill(MetaObject metaObject) { + + } + + @Override + public void updateFill(MetaObject metaObject) { + + } + +} diff --git a/blade-core-mybatis/src/main/java/org/springblade/core/mp/base/BaseEntity.java b/blade-core-mybatis/src/main/java/org/springblade/core/mp/base/BaseEntity.java new file mode 100644 index 0000000..0fc282c --- /dev/null +++ b/blade-core-mybatis/src/main/java/org/springblade/core/mp/base/BaseEntity.java @@ -0,0 +1,78 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.mp.base; + + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import org.springblade.core.tool.date.DatePattern; +import org.springframework.format.annotation.DateTimeFormat; + +import java.io.Serializable; +import java.time.LocalDateTime; + +@Data +public class BaseEntity implements Serializable { + /** + * 主键id + */ + @TableId(value = "id", type = IdType.AUTO) + @ApiModelProperty(value = "主键id") + private Integer id; + + /** + * 创建人 + */ + @ApiModelProperty(value = "创建人") + private Integer createUser; + + /** + * 创建时间 + */ + @DateTimeFormat(pattern = DatePattern.NORM_DATETIME_PATTERN) + @JsonFormat(pattern = DatePattern.NORM_DATETIME_PATTERN) + @ApiModelProperty(value = "创建时间") + private LocalDateTime createTime; + + /** + * 更新人 + */ + @ApiModelProperty(value = "更新人") + private Integer updateUser; + + /** + * 更新时间 + */ + @DateTimeFormat(pattern = DatePattern.NORM_DATETIME_PATTERN) + @JsonFormat(pattern = DatePattern.NORM_DATETIME_PATTERN) + @ApiModelProperty(value = "更新时间") + private LocalDateTime updateTime; + + /** + * 状态[1:正常] + */ + @ApiModelProperty(value = "业务状态") + private Integer status; + + /** + * 状态[0:未删除,1:删除] + */ + @ApiModelProperty(value = "是否已删除") + private Integer isDeleted; +} diff --git a/blade-core-mybatis/src/main/java/org/springblade/core/mp/base/BaseService.java b/blade-core-mybatis/src/main/java/org/springblade/core/mp/base/BaseService.java new file mode 100644 index 0000000..5210240 --- /dev/null +++ b/blade-core-mybatis/src/main/java/org/springblade/core/mp/base/BaseService.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.mp.base; + +import com.baomidou.mybatisplus.extension.service.IService; + +import javax.validation.constraints.NotEmpty; +import java.util.List; + +public interface BaseService extends IService { + + /** + * 逻辑删除 + * @param ids id集合(逗号分隔) + * @return + */ + boolean deleteLogic(@NotEmpty List ids); + +} diff --git a/blade-core-mybatis/src/main/java/org/springblade/core/mp/base/BaseServiceImpl.java b/blade-core-mybatis/src/main/java/org/springblade/core/mp/base/BaseServiceImpl.java new file mode 100644 index 0000000..0e4ed6c --- /dev/null +++ b/blade-core-mybatis/src/main/java/org/springblade/core/mp/base/BaseServiceImpl.java @@ -0,0 +1,83 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.mp.base; + +import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import org.springblade.core.secure.BladeUser; +import org.springblade.core.secure.utils.SecureUtil; +import org.springblade.core.tool.constant.BladeConstant; +import org.springblade.core.tool.utils.BeanUtil; +import org.springframework.validation.annotation.Validated; + +import javax.validation.constraints.NotEmpty; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.time.LocalDateTime; +import java.util.List; + +/** + * 业务封装基础类 + * + * @param mapper + * @param model + */ +@Validated +public class BaseServiceImpl, T extends BaseEntity> extends ServiceImpl implements BaseService { + + private Class modelClass; + + @SuppressWarnings("unchecked") + public BaseServiceImpl() { + Type type = this.getClass().getGenericSuperclass(); + this.modelClass = (Class) ((ParameterizedType) type).getActualTypeArguments()[1]; + } + + @Override + public boolean save(T entity) { + BladeUser user = SecureUtil.getUser(); + LocalDateTime now = LocalDateTime.now(); + entity.setCreateUser(user.getUserId()); + entity.setCreateTime(now); + entity.setUpdateUser(user.getUserId()); + entity.setUpdateTime(now); + entity.setStatus(BladeConstant.DB_STATUS_NORMAL); + entity.setIsDeleted(BladeConstant.DB_NOT_DELETED); + return super.save(entity); + } + + @Override + public boolean updateById(T entity) { + BladeUser user = SecureUtil.getUser(); + entity.setUpdateUser(user.getUserId()); + entity.setUpdateTime(LocalDateTime.now()); + return super.updateById(entity); + } + + @Override + public boolean deleteLogic(@NotEmpty List ids) { + BladeUser user = SecureUtil.getUser(); + T entity = BeanUtil.newInstance(modelClass); + entity.setUpdateUser(user.getUserId()); + entity.setUpdateTime(LocalDateTime.now()); + entity.setIsDeleted(BladeConstant.DB_IS_DELETED); + UpdateWrapper uw = new UpdateWrapper<>(); + uw.in(BladeConstant.DB_PRIMARY_KEY, ids); + return super.update(entity, uw); + } + +} diff --git a/blade-core-mybatis/src/main/java/org/springblade/core/mp/support/BaseEntityWrapper.java b/blade-core-mybatis/src/main/java/org/springblade/core/mp/support/BaseEntityWrapper.java new file mode 100644 index 0000000..d63a4ce --- /dev/null +++ b/blade-core-mybatis/src/main/java/org/springblade/core/mp/support/BaseEntityWrapper.java @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.mp.support; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * 视图包装基类 + */ +public abstract class BaseEntityWrapper { + + public abstract V entityVO(E entity); + + public IPage pageVO(IPage pages) { + List records = pages.getRecords().stream().map(this::entityVO).collect(Collectors.toList()); + IPage pageVo = new Page<>(pages.getCurrent(), pages.getSize(), pages.getTotal()); + pageVo.setRecords(records); + return pageVo; + } + +} diff --git a/blade-core-mybatis/src/main/java/org/springblade/core/mp/support/Condition.java b/blade-core-mybatis/src/main/java/org/springblade/core/mp/support/Condition.java new file mode 100644 index 0000000..5bb184e --- /dev/null +++ b/blade-core-mybatis/src/main/java/org/springblade/core/mp/support/Condition.java @@ -0,0 +1,74 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.mp.support; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.springblade.core.tool.utils.Func; +import org.springblade.core.tool.utils.BeanUtil; + +import java.util.Map; + +/** + * 分页工具 + */ +public class Condition { + + /** + * 转化成mybatis plus中的Page + * + * @param query + * @return + */ + public static IPage getPage(Query query) { + Page page = new Page<>(Func.toInt(query.getCurrent(), 1), Func.toInt(query.getSize(), 10)); + page.setAsc(Func.toStrArray(query.getAscs())); + page.setDesc(Func.toStrArray(query.getDescs())); + return page; + } + + /** + * 获取mybatis plus中的QueryWrapper + * + * @param entity + * @param + * @return + */ + public static QueryWrapper getQueryWrapper(T entity) { + return new QueryWrapper<>(entity); + } + + /** + * 获取mybatis plus中的QueryWrapper + * + * @param query + * @param clazz + * @param + * @return + */ + public static QueryWrapper getQueryWrapper(Map query, Class clazz) { + QueryWrapper qw = new QueryWrapper<>(); + qw.setEntity(BeanUtil.newInstance(clazz)); + if (Func.isNotEmpty(query)) { + query.forEach((k, v) -> { + if (Func.isNotEmpty(v)) qw.like(k, v); + }); + } + return qw; + } + +} diff --git a/blade-core-mybatis/src/main/java/org/springblade/core/mp/support/Query.java b/blade-core-mybatis/src/main/java/org/springblade/core/mp/support/Query.java new file mode 100644 index 0000000..f265e3f --- /dev/null +++ b/blade-core-mybatis/src/main/java/org/springblade/core/mp/support/Query.java @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.mp.support; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +/** + * 分页工具 + */ +@Data +@ApiModel(description = "查询条件") +public class Query { + + /** + * 当前页 + */ + @ApiModelProperty(value = "当前页") + private Integer current; + + /** + * 每页的数量 + */ + @ApiModelProperty(value = "每页的数量") + private Integer size; + + /** + * 排序的字段名 + */ + @ApiModelProperty(value = "升序字段") + private String ascs; + + /** + * 排序方式 + */ + @ApiModelProperty(value = "降序字段") + private String descs; + +} diff --git a/blade-core-secure/pom.xml b/blade-core-secure/pom.xml new file mode 100644 index 0000000..bde8e60 --- /dev/null +++ b/blade-core-secure/pom.xml @@ -0,0 +1,33 @@ + + + + blade-tool + org.springblade + 1.0.0-RC1 + + + 4.0.0 + + blade-core-secure + ${project.artifactId} + ${blade.tool.version} + jar + + + + + io.jsonwebtoken + jjwt + 0.9.1 + + + + org.springblade + blade-core-tool + ${blade.tool.version} + + + + diff --git a/blade-core-secure/src/main/java/org/springblade/core/secure/AuthInfo.java b/blade-core-secure/src/main/java/org/springblade/core/secure/AuthInfo.java new file mode 100644 index 0000000..0df84f8 --- /dev/null +++ b/blade-core-secure/src/main/java/org/springblade/core/secure/AuthInfo.java @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.secure; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +/** + * AuthInfo + */ +@Data +@ApiModel(description = "认证信息") +public class AuthInfo { + @ApiModelProperty(value = "令牌") + private String accessToken; + @ApiModelProperty(value = "令牌类型") + private String tokenType; + @ApiModelProperty(value = "角色名") + private String authority; + @ApiModelProperty(value = "用户名") + private String userName; + @ApiModelProperty(value = "账号名") + private String account; + @ApiModelProperty(value = "过期时间") + private long expiresIn; + @ApiModelProperty(value = "许可证") + private String license = "made by blade"; +} diff --git a/blade-core-secure/src/main/java/org/springblade/core/secure/BladeUser.java b/blade-core-secure/src/main/java/org/springblade/core/secure/BladeUser.java new file mode 100644 index 0000000..421bea2 --- /dev/null +++ b/blade-core-secure/src/main/java/org/springblade/core/secure/BladeUser.java @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.secure; + +import lombok.Data; + +import java.io.Serializable; + +/** + * 用户实体 + */ +@Data +public class BladeUser implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 主键 + */ + private Integer userId; + /** + * 昵称 + */ + private String userName; + /** + * 账号 + */ + private String account; + /** + * 角色id + */ + private String roleId; + /** + * 角色名 + */ + private String roleName; + + +} diff --git a/blade-core-secure/src/main/java/org/springblade/core/secure/annotation/PreAuth.java b/blade-core-secure/src/main/java/org/springblade/core/secure/annotation/PreAuth.java new file mode 100644 index 0000000..baa1567 --- /dev/null +++ b/blade-core-secure/src/main/java/org/springblade/core/secure/annotation/PreAuth.java @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.secure.annotation; + +import java.lang.annotation.*; + +/** + * 权限注解 用于检查权限 规定访问权限 + * + * @example @PreAuth("#userVO.id<10") + * @example @PreAuth("hasRole(#test, #test1)") + * @example @PreAuth("hasPermission(#test) and @PreAuth.hasPermission(#test)") + */ +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +@Documented +public @interface PreAuth { + + /** + * Spring el + * 文档地址:https://docs.spring.io/spring/docs/4.3.16.RELEASE/spring-framework-reference/htmlsingle/#expressions-operators-logical + */ + String value(); + +} + diff --git a/blade-core-secure/src/main/java/org/springblade/core/secure/aspect/AuthAspect.java b/blade-core-secure/src/main/java/org/springblade/core/secure/aspect/AuthAspect.java new file mode 100644 index 0000000..4ba4c1d --- /dev/null +++ b/blade-core-secure/src/main/java/org/springblade/core/secure/aspect/AuthAspect.java @@ -0,0 +1,118 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.secure.aspect; + +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.reflect.MethodSignature; +import org.springblade.core.secure.annotation.PreAuth; +import org.springblade.core.secure.auth.AuthFun; +import org.springblade.core.secure.exception.SecureException; +import org.springblade.core.tool.api.ResultCode; +import org.springblade.core.tool.utils.ClassUtil; +import org.springblade.core.tool.utils.StringUtil; +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.expression.BeanFactoryResolver; +import org.springframework.core.MethodParameter; +import org.springframework.expression.Expression; +import org.springframework.expression.ExpressionParser; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.StandardEvaluationContext; + +import java.lang.reflect.Method; + +/** + * AOP 鉴权 + */ +@Aspect +public class AuthAspect implements ApplicationContextAware { + + /** + * 表达式处理 + */ + private static final ExpressionParser SPEL_PARSER = new SpelExpressionParser(); + + /** + * 切 方法 和 类上的 @PreAuth 注解 + * @param point 切点 + * @return Object + * @throws Throwable 没有权限的异常 + */ + @Around( + "@annotation(org.springblade.core.secure.annotation.PreAuth) || " + + "@within(org.springblade.core.secure.annotation.PreAuth)" + ) + public Object preAuth(ProceedingJoinPoint point) throws Throwable { + if (handleAuth(point)) { + return point.proceed(); + } + throw new SecureException(ResultCode.UN_AUTHORIZED); + } + + /** + * 处理权限 + * + * @param point 切点 + */ + private boolean handleAuth(ProceedingJoinPoint point) { + MethodSignature ms = (MethodSignature) point.getSignature(); + Method method = ms.getMethod(); + // 读取权限注解,优先方法上,没有则读取类 + PreAuth preAuth = ClassUtil.getAnnotation(method, PreAuth.class); + // 判断表达式 + String condition = preAuth.value(); + if (StringUtil.isNotBlank(condition)) { + Expression expression = SPEL_PARSER.parseExpression(condition); + // 方法参数值 + Object[] args = point.getArgs(); + StandardEvaluationContext context = getEvaluationContext(method, args); + return expression.getValue(context, Boolean.class); + } + return false; + } + + /** + * 获取方法上的参数 + * + * @param method 方法 + * @param args 变量 + * @return {SimpleEvaluationContext} + */ + private StandardEvaluationContext getEvaluationContext(Method method, Object[] args) { + // 初始化Sp el表达式上下文,并设置 AuthFun + StandardEvaluationContext context = new StandardEvaluationContext(new AuthFun()); + // 设置表达式支持spring bean + context.setBeanResolver(new BeanFactoryResolver(applicationContext)); + for (int i = 0; i < args.length; i++) { + // 读取方法参数 + MethodParameter methodParam = ClassUtil.getMethodParameter(method, i); + // 设置方法 参数名和值 为sp el变量 + context.setVariable(methodParam.getParameterName(), args[i]); + } + return context; + } + + private ApplicationContext applicationContext; + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } + +} diff --git a/blade-core-secure/src/main/java/org/springblade/core/secure/auth/AuthFun.java b/blade-core-secure/src/main/java/org/springblade/core/secure/auth/AuthFun.java new file mode 100644 index 0000000..6a597f4 --- /dev/null +++ b/blade-core-secure/src/main/java/org/springblade/core/secure/auth/AuthFun.java @@ -0,0 +1,75 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.secure.auth; + +import org.springblade.core.secure.utils.SecureUtil; +import org.springblade.core.tool.constant.RoleConstant; +import org.springblade.core.tool.utils.CollectionUtil; +import org.springblade.core.tool.utils.Func; +import org.springblade.core.tool.utils.StringUtil; + +/** + * 权限判断 + */ +public class AuthFun { + + /** + * 放行所有请求 + * + * @return {boolean} + */ + public boolean permitAll() { + return true; + } + + /** + * 只有超管角色才可访问 + * + * @return {boolean} + */ + public boolean denyAll() { + return hasRole(RoleConstant.ADMIN); + } + + /** + * 判断是否有该角色权限 + * + * @param role 单角色 + * @return {boolean} + */ + public boolean hasRole(String role) { + return hasAnyRole(role); + } + + /** + * 判断是否有该角色权限 + * + * @param role 角色集合 + * @return {boolean} + */ + public boolean hasAnyRole(String... role) { + String userRole = SecureUtil.getUser().getRoleName(); + if (StringUtil.isBlank(userRole)) { + return false; + } + String[] roles = Func.toStrArray(userRole); + if (CollectionUtil.contains(roles, role)) { + return true; + } + return false; + } + +} diff --git a/blade-core-secure/src/main/java/org/springblade/core/secure/config/RegistryConfiguration.java b/blade-core-secure/src/main/java/org/springblade/core/secure/config/RegistryConfiguration.java new file mode 100644 index 0000000..1b98956 --- /dev/null +++ b/blade-core-secure/src/main/java/org/springblade/core/secure/config/RegistryConfiguration.java @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.secure.config; + + +import org.springblade.core.secure.registry.SecureRegistry; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * secure模块api放行默认配置 + */ +@Configuration +@AutoConfigureBefore(SecureConfiguration.class) +public class RegistryConfiguration { + + @Bean + @ConditionalOnMissingBean(SecureRegistry.class) + public SecureRegistry secureRegistry() { + return new SecureRegistry(); + } + +} diff --git a/blade-core-secure/src/main/java/org/springblade/core/secure/config/SecureConfiguration.java b/blade-core-secure/src/main/java/org/springblade/core/secure/config/SecureConfiguration.java new file mode 100644 index 0000000..3bc59e6 --- /dev/null +++ b/blade-core-secure/src/main/java/org/springblade/core/secure/config/SecureConfiguration.java @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.secure.config; + + +import lombok.AllArgsConstructor; +import org.springblade.core.secure.aspect.AuthAspect; +import org.springblade.core.secure.interceptor.SecureInterceptor; +import org.springblade.core.secure.registry.SecureRegistry; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Order +@Configuration +@AllArgsConstructor +public class SecureConfiguration implements WebMvcConfigurer { + + private final SecureRegistry secureRegistry; + + @Override + public void addInterceptors(InterceptorRegistry registry) { + if (secureRegistry.isEnable()) { + registry.addInterceptor(new SecureInterceptor()) + .excludePathPatterns(secureRegistry.getExcludePatterns()) + .excludePathPatterns(secureRegistry.getDefaultExcludePatterns()); + } + } + + @Bean + public AuthAspect authAspect() { + return new AuthAspect(); + } + +} diff --git a/blade-core-secure/src/main/java/org/springblade/core/secure/exception/SecureException.java b/blade-core-secure/src/main/java/org/springblade/core/secure/exception/SecureException.java new file mode 100644 index 0000000..898cf86 --- /dev/null +++ b/blade-core-secure/src/main/java/org/springblade/core/secure/exception/SecureException.java @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.secure.exception; + +import lombok.Getter; +import org.springblade.core.tool.api.IResultCode; +import org.springblade.core.tool.api.ResultCode; + +/** + * Secure异常 + */ +public class SecureException extends RuntimeException { + private static final long serialVersionUID = 2359767895161832954L; + + @Getter + private final IResultCode resultCode; + + public SecureException(String message) { + super(message); + this.resultCode = ResultCode.INTERNAL_SERVER_ERROR; + } + + public SecureException(IResultCode resultCode) { + super(resultCode.getMessage()); + this.resultCode = resultCode; + } + + public SecureException(IResultCode resultCode, Throwable cause) { + super(cause); + this.resultCode = resultCode; + } + + @Override + public Throwable fillInStackTrace() { + return this; + } +} diff --git a/blade-core-secure/src/main/java/org/springblade/core/secure/interceptor/SecureInterceptor.java b/blade-core-secure/src/main/java/org/springblade/core/secure/interceptor/SecureInterceptor.java new file mode 100644 index 0000000..0d8318f --- /dev/null +++ b/blade-core-secure/src/main/java/org/springblade/core/secure/interceptor/SecureInterceptor.java @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.secure.interceptor; + +import lombok.extern.slf4j.Slf4j; +import org.springblade.core.secure.utils.SecureUtil; +import org.springblade.core.tool.api.R; +import org.springblade.core.tool.api.ResultCode; +import org.springblade.core.tool.jackson.JsonUtil; +import org.springblade.core.tool.utils.StringPool; +import org.springblade.core.tool.utils.WebUtil; +import org.springframework.http.MediaType; +import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Objects; + +/** + * jwt拦截器校验 + */ +@Slf4j +public class SecureInterceptor extends HandlerInterceptorAdapter { + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + if (null != SecureUtil.getUser().getUserId()) { + return true; + } else { + log.warn("签名认证失败,请求接口:{},请求IP:{},请求参数:{}", request.getRequestURI(), WebUtil.getIP(request), JsonUtil.toJson(request.getParameterMap())); + R result = R.failure(ResultCode.UN_AUTHORIZED); + response.setCharacterEncoding(StringPool.UTF_8); + response.setHeader("Content-type", MediaType.APPLICATION_JSON_UTF8_VALUE); + response.setStatus(HttpServletResponse.SC_OK); + try { + response.getWriter().write(Objects.requireNonNull(JsonUtil.toJson(result))); + } catch (IOException ex) { + log.error(ex.getMessage()); + } + return false; + } + } + +} diff --git a/blade-core-secure/src/main/java/org/springblade/core/secure/registry/SecureRegistry.java b/blade-core-secure/src/main/java/org/springblade/core/secure/registry/SecureRegistry.java new file mode 100644 index 0000000..b15ce1c --- /dev/null +++ b/blade-core-secure/src/main/java/org/springblade/core/secure/registry/SecureRegistry.java @@ -0,0 +1,62 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.secure.registry; + +import lombok.Data; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * secure api放行配置 + */ +@Data +public class SecureRegistry { + + private boolean enable = true; + + private final List defaultExcludePatterns = new ArrayList<>(); + + private final List excludePatterns = new ArrayList<>(); + + public SecureRegistry() { + this.defaultExcludePatterns.add("/actuator/health/**"); + this.defaultExcludePatterns.add("/v2/api-docs/**"); + this.defaultExcludePatterns.add("/v2/api-docs-ext/**"); + this.defaultExcludePatterns.add("/auth/**"); + this.defaultExcludePatterns.add("/token/**"); + this.defaultExcludePatterns.add("/log/**"); + this.defaultExcludePatterns.add("/user/userInfo"); + this.defaultExcludePatterns.add("/error/**"); + } + + /** + * 设置放行api + */ + public SecureRegistry excludePathPatterns(String... patterns) { + return excludePathPatterns(Arrays.asList(patterns)); + } + + /** + * 设置放行api + */ + public SecureRegistry excludePathPatterns(List patterns) { + this.excludePatterns.addAll(patterns); + return this; + } + +} diff --git a/blade-core-secure/src/main/java/org/springblade/core/secure/utils/SecureUtil.java b/blade-core-secure/src/main/java/org/springblade/core/secure/utils/SecureUtil.java new file mode 100644 index 0000000..81d258b --- /dev/null +++ b/blade-core-secure/src/main/java/org/springblade/core/secure/utils/SecureUtil.java @@ -0,0 +1,184 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.secure.utils; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.JwtBuilder; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import org.springblade.core.secure.BladeUser; +import org.springblade.core.tool.date.DateField; +import org.springblade.core.tool.date.DateTime; +import org.springblade.core.tool.date.DateUtil; +import org.springblade.core.tool.utils.Func; +import org.springblade.core.tool.utils.WebUtil; + +import javax.crypto.spec.SecretKeySpec; +import javax.servlet.http.HttpServletRequest; +import javax.xml.bind.DatatypeConverter; +import java.security.Key; +import java.util.Date; +import java.util.Map; + +/** + * Secure工具类 + */ +public class SecureUtil { + + public final static String header = "Authorization"; + public final static String bearer = "bearer"; + public final static String account = "account"; + public final static String userId = "userId"; + public final static String roleId = "roleId"; + public final static String userName = "userName"; + public final static String roleName = "roleName"; + private static String base64Security = DatatypeConverter.printBase64Binary("SpringBlade".getBytes()); + + /** + * 获取用户信息 + * + * @return + */ + public static BladeUser getUser() { + return getUser(WebUtil.getRequest()); + } + + /** + * 获取用户信息 + * + * @return + */ + public static BladeUser getUser(HttpServletRequest request) { + Claims claims = getClaims(request); + if (claims == null) { + return new BladeUser(); + } + Integer userId = Func.toInt(claims.get(SecureUtil.userId)); + String roleId = Func.toStr(claims.get(SecureUtil.roleId)); + String account = Func.toStr(claims.get(SecureUtil.account)); + String roleName = Func.toStr(claims.get(SecureUtil.roleName)); + BladeUser bladeUser = new BladeUser(); + bladeUser.setAccount(account); + bladeUser.setUserId(userId); + bladeUser.setRoleId(roleId); + bladeUser.setRoleName(roleName); + return bladeUser; + } + + /** + * 获取Claims + * + * @return + */ + public static Claims getClaims(HttpServletRequest request) { + String auth = request.getHeader(SecureUtil.header); + if ((auth != null) && (auth.length() > 7)) { + String HeadStr = auth.substring(0, 6).toLowerCase(); + if (HeadStr.compareTo(SecureUtil.bearer) == 0) { + auth = auth.substring(7); + return SecureUtil.parseJWT(auth); + } + } + return null; + } + + /** + * 获取请求头 + * + * @return + */ + public static String getHeader() { + return getHeader(WebUtil.getRequest()); + } + + /** + * 获取请求头 + * + * @param request + * @return + */ + public static String getHeader(HttpServletRequest request) { + return request.getHeader(header); + } + + /** + * 解析jsonWebToken + * + * @param jsonWebToken + * @return + */ + public static Claims parseJWT(String jsonWebToken) { + try { + Claims claims = Jwts.parser() + .setSigningKey(DatatypeConverter.parseBase64Binary(base64Security)) + .parseClaimsJws(jsonWebToken).getBody(); + return claims; + } catch (Exception ex) { + return null; + } + } + + /** + * 创建jwt + * + * @param user 用户 + * @param audience audience + * @param issuer issuer + * @param isExpire isExpire + * @return + */ + public static String createJWT(Map user, String audience, String issuer, boolean isExpire) { + SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; + + long nowMillis = System.currentTimeMillis(); + Date now = new Date(nowMillis); + + //生成签名密钥 + byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(base64Security); + Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName()); + + //添加构成JWT的类 + JwtBuilder builder = Jwts.builder().setHeaderParam("typ", "JsonWebToken") + .setIssuer(issuer) + .setAudience(audience) + .signWith(signatureAlgorithm, signingKey); + + //设置JWT参数 + user.forEach(builder::claim); + + //添加Token过期时间 + if (isExpire) { + long expMillis = nowMillis + getExpire(); + Date exp = new Date(expMillis); + builder.setExpiration(exp).setNotBefore(now); + } + + //生成JWT + return builder.compact(); + } + + /** + * 获取过期时间(次日凌晨3点) + * + * @return + */ + public static long getExpire() { + DateTime dateTime = DateUtil.endOfDay(new Date()); + DateTime offset = DateUtil.offset(dateTime, DateField.HOUR, 3); + + return offset.getTime() - System.currentTimeMillis(); + } +} diff --git a/blade-core-secure/src/main/resources/META-INF/spring-devtools.properties b/blade-core-secure/src/main/resources/META-INF/spring-devtools.properties new file mode 100644 index 0000000..9773f18 --- /dev/null +++ b/blade-core-secure/src/main/resources/META-INF/spring-devtools.properties @@ -0,0 +1 @@ +restart.include.blade-core-secure=/blade-core-secure[\\w-]+\.jar diff --git a/blade-core-secure/src/main/resources/META-INF/spring.factories b/blade-core-secure/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..4cf6dfd --- /dev/null +++ b/blade-core-secure/src/main/resources/META-INF/spring.factories @@ -0,0 +1,3 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + org.springblade.core.secure.config.RegistryConfiguration,\ + org.springblade.core.secure.config.SecureConfiguration diff --git a/blade-core-swagger/pom.xml b/blade-core-swagger/pom.xml new file mode 100644 index 0000000..9e3d5b0 --- /dev/null +++ b/blade-core-swagger/pom.xml @@ -0,0 +1,49 @@ + + + + blade-tool + org.springblade + 1.0.0-RC1 + + + 4.0.0 + + blade-core-swagger + ${project.artifactId} + ${blade.tool.version} + jar + + + + + org.springblade + blade-core-tool + ${blade.tool.version} + + + + io.springfox + springfox-swagger2 + ${swagger.version} + + + io.swagger + swagger-models + + + + + io.swagger + swagger-models + ${swagger.models.version} + + + com.github.xiaoymin + swagger-bootstrap-ui + ${swagger.bootstrapui.version} + + + + diff --git a/blade-core-swagger/src/main/java/org/springblade/core/swagger/SwaggerAutoConfiguration.java b/blade-core-swagger/src/main/java/org/springblade/core/swagger/SwaggerAutoConfiguration.java new file mode 100644 index 0000000..f92a732 --- /dev/null +++ b/blade-core-swagger/src/main/java/org/springblade/core/swagger/SwaggerAutoConfiguration.java @@ -0,0 +1,134 @@ +/** + * Copyright (c) 2018-2028, lengleng (wangiegie@gmail.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.swagger; + + +import com.github.xiaoymin.swaggerbootstrapui.annotations.EnableSwaggerBootstrapUI; +import com.google.common.base.Predicate; +import com.google.common.base.Predicates; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import springfox.documentation.builders.ApiInfoBuilder; +import springfox.documentation.builders.PathSelectors; +import springfox.documentation.builders.RequestHandlerSelectors; +import springfox.documentation.service.*; +import springfox.documentation.spi.DocumentationType; +import springfox.documentation.spi.service.contexts.SecurityContext; +import springfox.documentation.spring.web.plugins.Docket; +import springfox.documentation.swagger2.annotations.EnableSwagger2; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * swagger配置 + */ +@Configuration +@EnableSwagger2 +@EnableSwaggerBootstrapUI +@Profile({"dev", "test"}) +public class SwaggerAutoConfiguration { + + private static final String DEFAULT_EXCLUDE_PATH = "/error"; + private static final String BASE_PATH = "/**"; + + @Bean + @ConditionalOnMissingBean + public SwaggerProperties swaggerProperties() { + return new SwaggerProperties(); + } + + @Bean + public Docket api(SwaggerProperties swaggerProperties) { + // base-path处理 + if (swaggerProperties.getBasePath().size() == 0) { + swaggerProperties.getBasePath().add(BASE_PATH); + } + //noinspection unchecked + List> basePath = new ArrayList(); + swaggerProperties.getBasePath().forEach(path -> basePath.add(PathSelectors.ant(path))); + + // exclude-path处理 + if (swaggerProperties.getExcludePath().size() == 0) { + swaggerProperties.getExcludePath().add(DEFAULT_EXCLUDE_PATH); + } + List> excludePath = new ArrayList<>(); + swaggerProperties.getExcludePath().forEach(path -> excludePath.add(PathSelectors.ant(path))); + + //noinspection Guava + return new Docket(DocumentationType.SWAGGER_2) + .host(swaggerProperties.getHost()) + .apiInfo(apiInfo(swaggerProperties)).select() + .apis(RequestHandlerSelectors.basePackage(swaggerProperties.getBasePackage())) + .paths(Predicates.and(Predicates.not(Predicates.or(excludePath)), Predicates.or(basePath))) + .build() + .securitySchemes(Collections.singletonList(securitySchema())) + .securityContexts(Collections.singletonList(securityContext())) + .pathMapping("/" ); + } + + /** + * 配置默认的全局鉴权策略的开关,通过正则表达式进行匹配;默认匹配所有URL + * + * @return + */ + private SecurityContext securityContext() { + return SecurityContext.builder() + .securityReferences(defaultAuth()) + .forPaths(PathSelectors.regex(swaggerProperties().getAuthorization().getAuthRegex())) + .build(); + } + + /** + * 默认的全局鉴权策略 + * + * @return + */ + private List defaultAuth() { + ArrayList authorizationScopeList = new ArrayList<>(); + swaggerProperties().getAuthorization().getAuthorizationScopeList().forEach(authorizationScope -> authorizationScopeList.add(new AuthorizationScope(authorizationScope.getScope(), authorizationScope.getDescription()))); + AuthorizationScope[] authorizationScopes = new AuthorizationScope[authorizationScopeList.size()]; + return Collections.singletonList(SecurityReference.builder() + .reference(swaggerProperties().getAuthorization().getName()) + .scopes(authorizationScopeList.toArray(authorizationScopes)) + .build()); + } + + + private OAuth securitySchema() { + ArrayList authorizationScopeList = new ArrayList<>(); + swaggerProperties().getAuthorization().getAuthorizationScopeList().forEach(authorizationScope -> authorizationScopeList.add(new AuthorizationScope(authorizationScope.getScope(), authorizationScope.getDescription()))); + ArrayList grantTypes = new ArrayList<>(); + swaggerProperties().getAuthorization().getTokenUrlList().forEach(tokenUrl -> grantTypes.add(new ResourceOwnerPasswordCredentialsGrant(tokenUrl))); + return new OAuth(swaggerProperties().getAuthorization().getName(), authorizationScopeList, grantTypes); + } + + private ApiInfo apiInfo(SwaggerProperties swaggerProperties) { + return new ApiInfoBuilder() + .title(swaggerProperties.getTitle()) + .description(swaggerProperties.getDescription()) + .license(swaggerProperties.getLicense()) + .licenseUrl(swaggerProperties.getLicenseUrl()) + .termsOfServiceUrl(swaggerProperties.getTermsOfServiceUrl()) + .contact(new Contact(swaggerProperties.getContact().getName(), swaggerProperties.getContact().getUrl(), swaggerProperties.getContact().getEmail())) + .version(swaggerProperties.getVersion()) + .build(); + } + +} diff --git a/blade-core-swagger/src/main/java/org/springblade/core/swagger/SwaggerProperties.java b/blade-core-swagger/src/main/java/org/springblade/core/swagger/SwaggerProperties.java new file mode 100644 index 0000000..3b546b0 --- /dev/null +++ b/blade-core-swagger/src/main/java/org/springblade/core/swagger/SwaggerProperties.java @@ -0,0 +1,139 @@ +/** + * Copyright (c) 2018-2028, lengleng (wangiegie@gmail.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.swagger; + +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.cloud.context.config.annotation.RefreshScope; + +import java.util.ArrayList; +import java.util.List; + +/** + * SwaggerProperties + */ +@Data +@RefreshScope +@ConfigurationProperties("swagger") +public class SwaggerProperties { + /** + * swagger会解析的包路径 + **/ + private String basePackage = "org.springblade"; + /** + * swagger会解析的url规则 + **/ + private List basePath = new ArrayList<>(); + /** + * 在basePath基础上需要排除的url规则 + **/ + private List excludePath = new ArrayList<>(); + /** + * 标题 + **/ + private String title = "SpringBlade 接口文档系统"; + /** + * 描述 + **/ + private String description = "SpringBlade 接口文档系统"; + /** + * 版本 + **/ + private String version = "1.0.0"; + /** + * 许可证 + **/ + private String license = ""; + /** + * 许可证URL + **/ + private String licenseUrl = ""; + /** + * 服务条款URL + **/ + private String termsOfServiceUrl = ""; + + /** + * host信息 + **/ + private String host = ""; + /** + * 联系人信息 + */ + private Contact contact = new Contact(); + /** + * 全局统一鉴权配置 + **/ + private Authorization authorization = new Authorization(); + + @Data + @NoArgsConstructor + public static class Contact { + + /** + * 联系人 + **/ + private String name = "chillzhuang"; + /** + * 联系人url + **/ + private String url = ""; + /** + * 联系人email + **/ + private String email = "smallchill@163.com"; + + } + + @Data + @NoArgsConstructor + public static class Authorization { + + /** + * 鉴权策略ID,需要和SecurityReferences ID保持一致 + */ + private String name = ""; + + /** + * 需要开启鉴权URL的正则 + */ + private String authRegex = "^.*$"; + + /** + * 鉴权作用域列表 + */ + private List authorizationScopeList = new ArrayList<>(); + + private List tokenUrlList = new ArrayList<>(); + } + + @Data + @NoArgsConstructor + public static class AuthorizationScope { + + /** + * 作用域名称 + */ + private String scope = ""; + + /** + * 作用域描述 + */ + private String description = ""; + + } +} diff --git a/blade-core-swagger/src/main/resources/META-INF/spring-configuration-metadata.json b/blade-core-swagger/src/main/resources/META-INF/spring-configuration-metadata.json new file mode 100644 index 0000000..5bbb770 --- /dev/null +++ b/blade-core-swagger/src/main/resources/META-INF/spring-configuration-metadata.json @@ -0,0 +1,136 @@ +{ + "hints": [], + "groups": [ + { + "sourceType": "org.springblade.core.swagger.SwaggerProperties", + "name": "swagger", + "type": "org.springblade.core.swagger.SwaggerProperties" + }, + { + "sourceType": "org.springblade.core.swagger.SwaggerProperties", + "name": "swagger.authorization", + "type": "org.springblade.core.swagger.SwaggerProperties$Authorization" + }, + { + "sourceType": "org.springblade.core.swagger.SwaggerProperties", + "name": "swagger.contact", + "type": "org.springblade.core.swagger.SwaggerProperties$Contact" + } + ], + "properties": [ + { + "sourceType": "org.springblade.core.swagger.SwaggerProperties$Authorization", + "defaultValue": "^.*$", + "name": "swagger.authorization.auth-regex", + "description": "需要开启鉴权URL的正则", + "type": "java.lang.String" + }, + { + "sourceType": "org.springblade.core.swagger.SwaggerProperties$Authorization", + "name": "swagger.authorization.authorization-scope-list", + "description": "鉴权作用域列表", + "type": "java.util.List" + }, + { + "sourceType": "org.springblade.core.swagger.SwaggerProperties$Authorization", + "defaultValue": "", + "name": "swagger.authorization.name", + "description": "鉴权策略ID,需要和SecurityReferences ID保持一致", + "type": "java.lang.String" + }, + { + "sourceType": "org.springblade.core.swagger.SwaggerProperties$Authorization", + "name": "swagger.authorization.token-url-list", + "type": "java.util.List" + }, + { + "sourceType": "org.springblade.core.swagger.SwaggerProperties", + "defaultValue": "org.springblade", + "name": "swagger.base-package", + "description": "swagger会解析的包路径", + "type": "java.lang.String" + }, + { + "sourceType": "org.springblade.core.swagger.SwaggerProperties", + "name": "swagger.base-path", + "description": "swagger会解析的url规则", + "type": "java.util.List" + }, + { + "sourceType": "org.springblade.core.swagger.SwaggerProperties$Contact", + "defaultValue": "smallchill@163.com", + "name": "swagger.contact.email", + "description": "联系人email", + "type": "java.lang.String" + }, + { + "sourceType": "org.springblade.core.swagger.SwaggerProperties$Contact", + "defaultValue": "chillzhuang", + "name": "swagger.contact.name", + "description": "联系人", + "type": "java.lang.String" + }, + { + "sourceType": "org.springblade.core.swagger.SwaggerProperties$Contact", + "defaultValue": "", + "name": "swagger.contact.url", + "description": "联系人url", + "type": "java.lang.String" + }, + { + "sourceType": "org.springblade.core.swagger.SwaggerProperties", + "defaultValue": "SpringBlade 接口文档系统", + "name": "swagger.description", + "description": "描述", + "type": "java.lang.String" + }, + { + "sourceType": "org.springblade.core.swagger.SwaggerProperties", + "name": "swagger.exclude-path", + "description": "在basePath基础上需要排除的url规则", + "type": "java.util.List" + }, + { + "sourceType": "org.springblade.core.swagger.SwaggerProperties", + "defaultValue": "", + "name": "swagger.host", + "description": "host信息", + "type": "java.lang.String" + }, + { + "sourceType": "org.springblade.core.swagger.SwaggerProperties", + "defaultValue": "", + "name": "swagger.license", + "description": "许可证", + "type": "java.lang.String" + }, + { + "sourceType": "org.springblade.core.swagger.SwaggerProperties", + "defaultValue": "", + "name": "swagger.license-url", + "description": "许可证URL", + "type": "java.lang.String" + }, + { + "sourceType": "org.springblade.core.swagger.SwaggerProperties", + "defaultValue": "", + "name": "swagger.terms-of-service-url", + "description": "服务条款URL", + "type": "java.lang.String" + }, + { + "sourceType": "org.springblade.core.swagger.SwaggerProperties", + "defaultValue": "SpringBlade 接口文档系统", + "name": "swagger.title", + "description": "标题", + "type": "java.lang.String" + }, + { + "sourceType": "org.springblade.core.swagger.SwaggerProperties", + "defaultValue": "1.0.0", + "name": "swagger.version", + "description": "版本", + "type": "java.lang.String" + } + ] +} diff --git a/blade-core-swagger/src/main/resources/META-INF/spring.factories b/blade-core-swagger/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..7af0f77 --- /dev/null +++ b/blade-core-swagger/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + org.springblade.core.swagger.SwaggerAutoConfiguration diff --git a/blade-core-tool/pom.xml b/blade-core-tool/pom.xml new file mode 100644 index 0000000..f122d4e --- /dev/null +++ b/blade-core-tool/pom.xml @@ -0,0 +1,77 @@ + + + + + org.springblade + blade-tool + 1.0.0-RC1 + + + 4.0.0 + + blade-core-tool + ${project.artifactId} + ${blade.tool.version} + jar + + + + + org.springblade + blade-core-launch + ${blade.tool.version} + + + + org.springframework.cloud + spring-cloud-commons + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + + + com.google.guava + guava + 26.0-jre + + + + com.squareup.okhttp3 + okhttp + 3.11.0 + + + + io.springfox + springfox-swagger2 + ${swagger.version} + + + io.swagger + swagger-models + + + + + io.swagger + swagger-models + ${swagger.models.version} + + + + io.protostuff + protostuff-core + ${protostuff.version} + + + io.protostuff + protostuff-runtime + ${protostuff.version} + + + + diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/api/IResultCode.java b/blade-core-tool/src/main/java/org/springblade/core/tool/api/IResultCode.java new file mode 100644 index 0000000..4317929 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/api/IResultCode.java @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.tool.api; + +import java.io.Serializable; + +/** + * 业务代码接口 + */ +public interface IResultCode extends Serializable { + + String getMessage(); + + int getCode(); + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/api/R.java b/blade-core-tool/src/main/java/org/springblade/core/tool/api/R.java new file mode 100644 index 0000000..b19d01a --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/api/R.java @@ -0,0 +1,188 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.tool.api; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.*; +import org.springblade.core.tool.constant.BladeConstant; +import org.springblade.core.tool.utils.ObjectUtil; +import org.springframework.lang.Nullable; + +import javax.servlet.http.HttpServletResponse; +import java.io.Serializable; +import java.util.Optional; + +/** + * 统一API响应结果封装 + */ +@Getter +@Setter +@ToString +@ApiModel(description = "返回信息") +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class R implements Serializable { + + private static final long serialVersionUID = 1L; + + @ApiModelProperty(value = "状态码", required = true) + private int code; + @ApiModelProperty(value = "是否成功", required = true) + private boolean success; + @ApiModelProperty(value = "承载数据") + private T data; + @ApiModelProperty(value = "返回消息", required = true) + private String msg; + + private R(IResultCode resultCode) { + this(resultCode, null, resultCode.getMessage()); + } + + private R(IResultCode resultCode, String msg) { + this(resultCode, null, msg); + } + + private R(IResultCode resultCode, T data) { + this(resultCode, data, resultCode.getMessage()); + } + + private R(IResultCode resultCode, T data, String msg) { + this(resultCode.getCode(), data, msg); + } + + private R(int code, T data, String msg) { + this.code = code; + this.data = data; + this.msg = msg; + this.success = ResultCode.SUCCESS.code == code; + } + + /** + * 判断返回是否为成功 + * + * @param result Result + * @return 是否成功 + */ + public static boolean isSuccess(@Nullable R result) { + return Optional.ofNullable(result) + .map(x -> ObjectUtil.nullSafeEquals(ResultCode.SUCCESS.code, x.code)) + .orElse(Boolean.FALSE); + } + + /** + * 判断返回是否为成功 + * + * @param result Result + * @return 是否成功 + */ + public static boolean isNotSuccess(@Nullable R result) { + return !R.isSuccess(result); + } + + /** + * 返回R + * @param data 数据 + */ + public static R data(T data) { + return data(data, BladeConstant.DEFAULT_SUCCESS_MESSAGE); + } + + /** + * 返回R + * @param data 数据 + * @param msg 消息 + */ + public static R data(T data, String msg) { + return data(HttpServletResponse.SC_OK, data, msg); + } + + /** + * 返回R + * @param code 状态码 + * @param data 数据 + * @param msg 消息 + */ + public static R data(int code, T data, String msg) { + return new R<>(code, data, data == null ? BladeConstant.DEFAULT_NULL_MESSAGE : msg); + } + + /** + * 返回R + * @param msg 消息 + */ + public static R success(String msg) { + return new R<>(ResultCode.SUCCESS, msg); + } + + /** + * 返回R + * @param resultCode 业务代码 + */ + public static R success(IResultCode resultCode) { + return new R<>(resultCode); + } + + /** + * 返回R + * @param resultCode 业务代码 + */ + public static R success(IResultCode resultCode, String msg) { + return new R<>(resultCode, msg); + } + + /** + * 返回R + * @param msg 消息 + */ + public static R failure(String msg) { + return new R<>(ResultCode.FAILURE, msg); + } + + + /** + * 返回R + * @param code 状态码 + * @param msg 消息 + */ + public static R failure(int code, String msg) { + return new R<>(code, null, msg); + } + + /** + * 返回R + * @param resultCode 业务代码 + */ + public static R failure(IResultCode resultCode) { + return new R<>(resultCode); + } + + /** + * 返回R + * @param resultCode 业务代码 + */ + public static R failure(IResultCode resultCode, String msg) { + return new R<>(resultCode, msg); + } + + /** + * 返回R + * @param flag 成功状态 + */ + public static R status(boolean flag) { + return flag ? success(BladeConstant.DEFAULT_SUCCESS_MESSAGE) : failure(BladeConstant.DEFAULT_FAILURE_MESSAGE); + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/api/ResultCode.java b/blade-core-tool/src/main/java/org/springblade/core/tool/api/ResultCode.java new file mode 100644 index 0000000..4841052 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/api/ResultCode.java @@ -0,0 +1,105 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.tool.api; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import javax.servlet.http.HttpServletResponse; + +/** + * 业务代码枚举 + */ +@Getter +@AllArgsConstructor +public enum ResultCode implements IResultCode { + + /** + * 操作成功 + */ + SUCCESS(HttpServletResponse.SC_OK, "操作成功"), + + /** + * 业务异常 + */ + FAILURE(HttpServletResponse.SC_BAD_REQUEST, "业务异常"), + + /** + * 请求未授权 + */ + UN_AUTHORIZED(HttpServletResponse.SC_UNAUTHORIZED, "请求未授权"), + + /** + * 404 没找到请求 + */ + NOT_FOUND(HttpServletResponse.SC_NOT_FOUND, "404 没找到请求"), + + /** + * 消息不能读取 + */ + MSG_NOT_READABLE(HttpServletResponse.SC_BAD_REQUEST, "消息不能读取"), + + /** + * 不支持当前请求方法 + */ + METHOD_NOT_SUPPORTED(HttpServletResponse.SC_METHOD_NOT_ALLOWED, "不支持当前请求方法"), + + /** + * 不支持当前媒体类型 + */ + MEDIA_TYPE_NOT_SUPPORTED(HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE, "不支持当前媒体类型"), + + /** + * 请求被拒绝 + */ + REQ_REJECT(HttpServletResponse.SC_FORBIDDEN, "请求被拒绝"), + + /** + * 服务器异常 + */ + INTERNAL_SERVER_ERROR(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "服务器异常"), + + /** + * 缺少必要的请求参数 + */ + PARAM_MISS(HttpServletResponse.SC_BAD_REQUEST, "缺少必要的请求参数"), + + /** + * 请求参数类型错误 + */ + PARAM_TYPE_ERROR(HttpServletResponse.SC_BAD_REQUEST, "请求参数类型错误"), + + /** + * 请求参数绑定错误 + */ + PARAM_BIND_ERROR(HttpServletResponse.SC_BAD_REQUEST, "请求参数绑定错误"), + + /** + * 参数校验失败 + */ + PARAM_VALID_ERROR(HttpServletResponse.SC_BAD_REQUEST, "参数校验失败"), + ; + + /** + * code编码 + */ + final int code; + /** + * 中文信息描述 + */ + final String message; + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/config/JacksonConfiguration.java b/blade-core-tool/src/main/java/org/springblade/core/tool/config/JacksonConfiguration.java new file mode 100644 index 0000000..1fec89b --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/config/JacksonConfiguration.java @@ -0,0 +1,73 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.tool.config; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import org.springblade.core.tool.date.DatePattern; +import org.springblade.core.tool.jackson.BladeJavaTimeModule; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; + +import java.text.SimpleDateFormat; +import java.time.ZoneId; +import java.util.Locale; +import java.util.TimeZone; + +@Configuration +@ConditionalOnClass(ObjectMapper.class) +@AutoConfigureBefore(JacksonAutoConfiguration.class) +public class JacksonConfiguration { + + @Primary + @Bean + public ObjectMapper objectMapper(Jackson2ObjectMapperBuilder builder) { + builder.simpleDateFormat(DatePattern.NORM_DATETIME_PATTERN); + //创建ObjectMapper + ObjectMapper objectMapper = builder.createXmlMapper(false).build(); + //设置地点为中国 + objectMapper.setLocale(Locale.CHINA); + //去掉默认的时间戳格式 + objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); + //设置为中国上海时区 + objectMapper.setTimeZone(TimeZone.getTimeZone(ZoneId.systemDefault())); + //序列化时,日期的统一格式 + objectMapper.setDateFormat(new SimpleDateFormat(DatePattern.NORM_DATETIME_PATTERN, Locale.CHINA)); + //序列化处理 + objectMapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_CONTROL_CHARS, true); + objectMapper.configure(JsonParser.Feature.ALLOW_BACKSLASH_ESCAPING_ANY_CHARACTER, true); + objectMapper.findAndRegisterModules(); + //失败处理 + objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); + objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + //单引号处理 + objectMapper.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true); + //反序列化时,属性不存在的兼容处理 + objectMapper.getDeserializationConfig().withoutFeatures(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + //日期格式化 + objectMapper.registerModule(new BladeJavaTimeModule()); + objectMapper.findAndRegisterModules(); + return objectMapper; + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/config/MessageConfiguration.java b/blade-core-tool/src/main/java/org/springblade/core/tool/config/MessageConfiguration.java new file mode 100644 index 0000000..9a397c6 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/config/MessageConfiguration.java @@ -0,0 +1,70 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.tool.config; + + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.AllArgsConstructor; +import org.springblade.core.tool.jackson.MappingApiJackson2HttpMessageConverter; +import org.springblade.core.tool.support.xss.XssFilter; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.http.converter.*; +import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import javax.servlet.DispatcherType; +import java.nio.charset.StandardCharsets; +import java.util.List; + +@Configuration +@AllArgsConstructor +@Order(Ordered.HIGHEST_PRECEDENCE) +public class MessageConfiguration implements WebMvcConfigurer { + + private final ObjectMapper objectMapper; + + /** + * 使用 JACKSON 作为JSON MessageConverter + */ + @Override + public void configureMessageConverters(List> converters) { + converters.removeIf(x -> x instanceof StringHttpMessageConverter || x instanceof AbstractJackson2HttpMessageConverter); + converters.add(new StringHttpMessageConverter(StandardCharsets.UTF_8)); + converters.add(new ByteArrayHttpMessageConverter()); + converters.add(new ResourceHttpMessageConverter()); + converters.add(new ResourceRegionHttpMessageConverter()); + converters.add(new MappingApiJackson2HttpMessageConverter(objectMapper)); + } + + /** + * 防XSS注入 + */ + @Bean + public FilterRegistrationBean xssFilterRegistration() { + FilterRegistrationBean registration = new FilterRegistrationBean(); + registration.setDispatcherTypes(DispatcherType.REQUEST); + registration.setFilter(new XssFilter()); + registration.addUrlPatterns("/*"); + registration.setName("xssFilter"); + registration.setOrder(Ordered.LOWEST_PRECEDENCE); + return registration; + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/config/ToolConfiguration.java b/blade-core-tool/src/main/java/org/springblade/core/tool/config/ToolConfiguration.java new file mode 100644 index 0000000..da6103c --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/config/ToolConfiguration.java @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.tool.config; + + +import org.springblade.core.tool.utils.SpringUtil; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + + +@Configuration +@Order(Ordered.HIGHEST_PRECEDENCE) +public class ToolConfiguration implements WebMvcConfigurer { + + /** + * Spring上下文缓存 + */ + @Bean + public SpringUtil springUtils() { + return new SpringUtil(); + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/constant/BladeConstant.java b/blade-core-tool/src/main/java/org/springblade/core/tool/constant/BladeConstant.java new file mode 100644 index 0000000..8fdcc80 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/constant/BladeConstant.java @@ -0,0 +1,83 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.tool.constant; + +/** + * 系统常量 + */ +public interface BladeConstant { + + /** + * 编码 + */ + String UTF_8 = "UTF-8"; + + /** + * JSON 资源 + */ + String CONTENT_TYPE = "application/json; charset=utf-8"; + + /** + * 角色前缀 + */ + String SECURITY_ROLE_PREFIX = "ROLE_"; + + /** + * 主键字段名 + */ + String DB_PRIMARY_KEY = "id"; + + /** + * 业务状态[1:正常] + */ + int DB_STATUS_NORMAL = 1; + + + /** + * 删除状态[0:正常,1:删除] + */ + int DB_NOT_DELETED = 0; + int DB_IS_DELETED = 1; + + /** + * 用户锁定状态 + */ + int DB_ADMIN_NON_LOCKED = 0; + int DB_ADMIN_LOCKED = 1; + + /** + * 日志默认状态 + */ + String LOG_NORMAL_TYPE = "1"; + + /** + * 默认为空消息 + */ + String DEFAULT_NULL_MESSAGE = "暂无承载数据"; + /** + * 默认成功消息 + */ + String DEFAULT_SUCCESS_MESSAGE = "操作成功"; + /** + * 默认失败消息 + */ + String DEFAULT_FAILURE_MESSAGE = "操作失败"; + /** + * 默认未授权消息 + */ + String DEFAULT_UNAUTHORIZED_MESSAGE = "签名认证失败"; + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/constant/RoleConstant.java b/blade-core-tool/src/main/java/org/springblade/core/tool/constant/RoleConstant.java new file mode 100644 index 0000000..a80620f --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/constant/RoleConstant.java @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.tool.constant; + +/** + * 系统默认角色 + */ +public class RoleConstant { + + public static final String ADMIN = "admin"; + + public static final String HAS_ROLE_ADMIN = "hasRole('" + ADMIN + "')"; + + public static final String USER = "user"; + + public static final String HAS_ROLE_USER = "hasRole('" + USER + "')"; + + public static final String TEST = "test"; + + public static final String HAS_ROLE_TEST = "hasRole('" + TEST + "')"; + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/constant/SystemConstant.java b/blade-core-tool/src/main/java/org/springblade/core/tool/constant/SystemConstant.java new file mode 100644 index 0000000..f01118f --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/constant/SystemConstant.java @@ -0,0 +1,100 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.tool.constant; + + +import lombok.Data; + +/** + * Blade系统配置类 + */ +@Data +public class SystemConstant { + + /** + * 开发模式 + */ + private boolean devMode = false; + + /** + * 远程上传模式 + */ + private boolean remoteMode = false; + + /** + * 外网地址 + */ + private String domain = "http://localhost:8888"; + + /** + * 上传下载路径(物理路径) + */ + private String remotePath = System.getProperty("user.dir") + "/target/blade"; + + /** + * 上传路径(相对路径) + */ + private String uploadPath = "/upload"; + + /** + * 下载路径 + */ + private String downloadPath = "/download"; + + /** + * 图片压缩 + */ + private boolean compress = false; + + /** + * 图片压缩比例 + */ + private Double compressScale = 2.00; + + /** + * 图片缩放选择:true放大;false缩小 + */ + private boolean compressFlag = false; + + /** + * 项目物理路径 + */ + private String realPath = System.getProperty("user.dir"); + + /** + * 项目相对路径 + */ + private String contextPath = "/"; + + private static final SystemConstant me = new SystemConstant(); + + private SystemConstant() { + + } + + public static SystemConstant me() { + return me; + } + + public String getUploadRealPath() { + return (remoteMode ? remotePath : realPath) + uploadPath; + } + + public String getUploadCtxPath() { + return contextPath + uploadPath; + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/date/BetweenFormater.java b/blade-core-tool/src/main/java/org/springblade/core/tool/date/BetweenFormater.java new file mode 100644 index 0000000..c3b32da --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/date/BetweenFormater.java @@ -0,0 +1,157 @@ +package org.springblade.core.tool.date; + +/** + * 时长格式化器 + * @author Looly + * + */ +public class BetweenFormater { + + /** 时长毫秒数 */ + private long betweenMs; + /** 格式化级别 */ + private Level level; + /** 格式化级别的最大个数 */ + private int levelMaxCount; + + /** + * 构造 + * @param betweenMs 日期间隔 + * @param level 级别,按照天、小时、分、秒、毫秒分为5个等级,根据传入等级,格式化到相应级别 + */ + public BetweenFormater(long betweenMs, Level level) { + this(betweenMs, level, 0); + } + + /** + * 构造 + * @param betweenMs 日期间隔 + * @param level 级别,按照天、小时、分、秒、毫秒分为5个等级,根据传入等级,格式化到相应级别 + * @param levelMaxCount 格式化级别的最大个数,假如级别个数为1,但是级别到秒,那只显示一个级别 + */ + public BetweenFormater(long betweenMs, Level level, int levelMaxCount) { + this.betweenMs = betweenMs; + this.level = level; + this.levelMaxCount = levelMaxCount; + } + + /** + * 格式化日期间隔输出
+ * + * @return 格式化后的字符串 + */ + public String format(){ + if(betweenMs == 0){ + return "0"; + } + + long day = betweenMs / DateUnit.DAY.getMillis(); + long hour = betweenMs / DateUnit.HOUR.getMillis() - day * 24; + long minute = betweenMs / DateUnit.MINUTE.getMillis() - day * 24 * 60 - hour * 60; + long second = betweenMs / DateUnit.SECOND.getMillis() - ((day * 24 + hour) * 60 + minute) * 60; + long millisecond = betweenMs - (((day * 24 + hour) * 60 + minute) * 60 + second) * 1000; + + StringBuilder sb = new StringBuilder(); + final int level = this.level.value; + int levelCount = 0; + + if(isLevelCountValid(levelCount) && 0 != day && level > 0){ + sb.append(day).append("天"); + levelCount++; + } + if(isLevelCountValid(levelCount) && 0 != hour && level > 1){ + sb.append(hour).append("小时"); + levelCount++; + } + if(isLevelCountValid(levelCount) && 0 != minute && level > 2){ + sb.append(minute).append("分"); + levelCount++; + } + if(isLevelCountValid(levelCount) && 0 != second && level > 3){ + sb.append(second).append("秒"); + levelCount++; + } + if(isLevelCountValid(levelCount) && 0 != millisecond && level > 4){ + sb.append(millisecond).append("毫秒"); + levelCount++; + } + + return sb.toString(); + } + + /** + * 获得 时长毫秒数 + * @return 时长毫秒数 + */ + public long getBetweenMs() { + return betweenMs; + } + + /** + * 设置 时长毫秒数 + * @param betweenMs 时长毫秒数 + */ + public void setBetweenMs(long betweenMs) { + this.betweenMs = betweenMs; + } + + /** + * 获得 格式化级别 + * @return 格式化级别 + */ + public Level getLevel() { + return level; + } + + /** + * 设置格式化级别 + * @param level 格式化级别 + */ + public void setLevel(Level level) { + this.level = level; + } + + /** + * 格式化等级枚举
+ * @author Looly + */ + public static enum Level { + + /** 天 */ + DAY(1), + /** 小时 */ + HOUR(2), + /** 分钟 */ + MINUTE(3), + /** 秒 */ + SECOND(4), + /** 毫秒 */ + MILLSECOND(5); + + private int value; + + private Level(int value) { + this.value = value; + } + + public int getValue() { + return this.value; + } + } + + @Override + public String toString() { + return format(); + } + + /** + * 等级数量是否有效
+ * 有效的定义是:levelMaxCount大于0(被设置),当前等级数量没有超过这个最大值 + * + * @param levelCount 登记数量 + * @return 是否有效 + */ + private boolean isLevelCountValid(int levelCount){ + return this.levelMaxCount <= 0 || levelCount < this.levelMaxCount; + } +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/date/ConcurrentDateFormat.java b/blade-core-tool/src/main/java/org/springblade/core/tool/date/ConcurrentDateFormat.java new file mode 100644 index 0000000..be576c1 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/date/ConcurrentDateFormat.java @@ -0,0 +1,70 @@ +package org.springblade.core.tool.date; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.Queue; +import java.util.TimeZone; +import java.util.concurrent.ConcurrentLinkedQueue; + +/** + * 参考tomcat8中的并发DateFormat + *

+ * {@link SimpleDateFormat}的线程安全包装器。 + * 不使用ThreadLocal,创建足够的SimpleDateFormat对象来满足并发性要求。 + * + * @author L.cm + */ +public class ConcurrentDateFormat { + private final String format; + private final Locale locale; + private final TimeZone timezone; + private final Queue queue = new ConcurrentLinkedQueue<>(); + + private ConcurrentDateFormat(String format, Locale locale, TimeZone timezone) { + this.format = format; + this.locale = locale; + this.timezone = timezone; + SimpleDateFormat initial = createInstance(); + queue.add(initial); + } + + public static ConcurrentDateFormat of(String format) { + return new ConcurrentDateFormat(format, Locale.getDefault(), TimeZone.getDefault()); + } + + public static ConcurrentDateFormat of(String format, TimeZone timezone) { + return new ConcurrentDateFormat(format, Locale.getDefault(), timezone); + } + + public static ConcurrentDateFormat of(String format, Locale locale, TimeZone timezone) { + return new ConcurrentDateFormat(format, locale, timezone); + } + + public String format(Date date) { + SimpleDateFormat sdf = queue.poll(); + if (sdf == null) { + sdf = createInstance(); + } + String result = sdf.format(date); + queue.add(sdf); + return result; + } + + public Date parse(String source) throws ParseException { + SimpleDateFormat sdf = queue.poll(); + if (sdf == null) { + sdf = createInstance(); + } + Date result = sdf.parse(source); + queue.add(sdf); + return result; + } + + private SimpleDateFormat createInstance() { + SimpleDateFormat sdf = new SimpleDateFormat(format, locale); + sdf.setTimeZone(timezone); + return sdf; + } +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/date/DateBetween.java b/blade-core-tool/src/main/java/org/springblade/core/tool/date/DateBetween.java new file mode 100644 index 0000000..d86a2f8 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/date/DateBetween.java @@ -0,0 +1,153 @@ +package org.springblade.core.tool.date; + +import java.util.Calendar; +import java.util.Date; + +/** + * 日期间隔 + * + * @author Looly + * + */ +public class DateBetween { + + /** 开始日期 */ + private Date begin; + /** 结束日期 */ + private Date end; + + /** + * 创建
+ * 在前的日期做为起始时间,在后的做为结束时间,间隔只保留绝对值正数 + * + * @param begin 起始时间 + * @param end 结束时间 + * @return {@link DateBetween} + * @since 3.2.3 + */ + public static DateBetween create(Date begin, Date end) { + return new DateBetween(begin, end); + } + + /** + * 创建
+ * 在前的日期做为起始时间,在后的做为结束时间,间隔只保留绝对值正数 + * + * @param begin 起始时间 + * @param end 结束时间 + * @param isAbs 日期间隔是否只保留绝对值正数 + * @return {@link DateBetween} + * @since 3.2.3 + */ + public static DateBetween create(Date begin, Date end, boolean isAbs) { + return new DateBetween(begin, end, isAbs); + } + + /** + * 构造
+ * 在前的日期做为起始时间,在后的做为结束时间,间隔只保留绝对值正数 + * + * @param begin 起始时间 + * @param end 结束时间 + */ + public DateBetween(Date begin, Date end) { + this(begin, end, true); + } + + /** + * 构造
+ * 在前的日期做为起始时间,在后的做为结束时间 + * + * @param begin 起始时间 + * @param end 结束时间 + * @param isAbs 日期间隔是否只保留绝对值正数 + * @since 3.1.1 + */ + public DateBetween(Date begin, Date end, boolean isAbs) { + if (isAbs && begin.after(end)) { + // 间隔只为正数的情况下,如果开始日期晚于结束日期,置换之 + this.begin = end; + this.end = begin; + } else { + this.begin = begin; + this.end = end; + } + } + + /** + * 判断两个日期相差的时长
+ * 返回 给定单位的时长差 + * + * @param unit 相差的单位:相差 天{@link DateUnit#DAY}、小时{@link DateUnit#HOUR} 等 + * @return 时长差 + */ + public long between(DateUnit unit) { + long diff = end.getTime() - begin.getTime(); + return diff / unit.getMillis(); + } + + /** + * 计算两个日期相差月数
+ * 在非重置情况下,如果起始日期的天小于结束日期的天,月数要少算1(不足1个月) + * + * @param isReset 是否重置时间为起始时间(重置天时分秒) + * @return 相差月数 + * @since 3.0.8 + */ + public long betweenMonth(boolean isReset) { + final Calendar beginCal = DateUtil.calendar(begin); + final Calendar endCal = DateUtil.calendar(end); + + final int betweenYear = endCal.get(Calendar.YEAR) - beginCal.get(Calendar.YEAR); + final int betweenMonthOfYear = endCal.get(Calendar.MONTH) - beginCal.get(Calendar.MONTH); + + int result = betweenYear * 12 + betweenMonthOfYear; + if (false == isReset) { + endCal.set(Calendar.YEAR, beginCal.get(Calendar.YEAR)); + endCal.set(Calendar.MONTH, beginCal.get(Calendar.MONTH)); + long between = endCal.getTimeInMillis() - beginCal.getTimeInMillis(); + if (between < 0) { + return result - 1; + } + } + return result; + } + + /** + * 计算两个日期相差年数
+ * 在非重置情况下,如果起始日期的月小于结束日期的月,年数要少算1(不足1年) + * + * @param isReset 是否重置时间为起始时间(重置月天时分秒) + * @return 相差年数 + * @since 3.0.8 + */ + public long betweenYear(boolean isReset) { + final Calendar beginCal = DateUtil.calendar(begin); + final Calendar endCal = DateUtil.calendar(end); + + int result = endCal.get(Calendar.YEAR) - beginCal.get(Calendar.YEAR); + if (false == isReset) { + endCal.set(Calendar.YEAR, beginCal.get(Calendar.YEAR)); + long between = endCal.getTimeInMillis() - beginCal.getTimeInMillis(); + if (between < 0) { + return result - 1; + } + } + return result; + } + + /** + * 格式化输出时间差
+ * + * @param level 级别 + * @return 字符串 + */ + public String toString(BetweenFormater.Level level) { + return DateUtil.formatBetween(between(DateUnit.MS), level); + } + + @Override + public String toString() { + return toString(BetweenFormater.Level.MILLSECOND); + } +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/date/DateException.java b/blade-core-tool/src/main/java/org/springblade/core/tool/date/DateException.java new file mode 100644 index 0000000..05faf83 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/date/DateException.java @@ -0,0 +1,31 @@ +package org.springblade.core.tool.date; + +import org.springblade.core.tool.utils.StringUtil; + +/** + * 工具类异常 + * @author xiaoleilu + */ +public class DateException extends RuntimeException{ + private static final long serialVersionUID = 8247610319171014183L; + + public DateException(Throwable e) { + super(StringUtil.format("{}: {}", e.getClass().getSimpleName(), e.getMessage()), e); + } + + public DateException(String message) { + super(message); + } + + public DateException(String messageTemplate, Object... params) { + super(StringUtil.format(messageTemplate, params)); + } + + public DateException(String message, Throwable throwable) { + super(message, throwable); + } + + public DateException(Throwable throwable, String messageTemplate, Object... params) { + super(StringUtil.format(messageTemplate, params), throwable); + } +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/date/DateField.java b/blade-core-tool/src/main/java/org/springblade/core/tool/date/DateField.java new file mode 100644 index 0000000..f53f982 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/date/DateField.java @@ -0,0 +1,130 @@ +package org.springblade.core.tool.date; + +import java.util.Calendar; + +/** + * 日期各个部分的枚举
+ * 与Calendar相应值对应 + * + * @author Looly + * + */ +public enum DateField { + + /** + * 年 + * @see Calendar#YEAR + */ + YEAR(Calendar.YEAR), + /** + * 月 + * @see Calendar#MONTH + */ + MONTH(Calendar.MONTH), + /** + * 一年中第几周 + * @see Calendar#WEEK_OF_YEAR + */ + WEEK_OF_YEAR(Calendar.WEEK_OF_YEAR), + /** + * 一月中第几周 + * @see Calendar#WEEK_OF_MONTH + */ + WEEK_OF_MONTH(Calendar.WEEK_OF_MONTH), + /** + * 一月中的第几天 + * @see Calendar#DAY_OF_MONTH + */ + DAY_OF_MONTH(Calendar.DAY_OF_MONTH), + /** + *一年中的第几天 + * @see Calendar#DAY_OF_YEAR + */ + DAY_OF_YEAR(Calendar.DAY_OF_YEAR), + /** + *周几,1表示周日,2表示周一 + * @see Calendar#DAY_OF_WEEK + */ + DAY_OF_WEEK(Calendar.DAY_OF_WEEK), + /** + * 天所在的周是这个月的第几周 + * @see Calendar#DAY_OF_WEEK_IN_MONTH + */ + DAY_OF_WEEK_IN_MONTH(Calendar.DAY_OF_WEEK_IN_MONTH), + /** + * 上午或者下午 + * @see Calendar#AM_PM + */ + AM_PM(Calendar.AM_PM), + /** + * 小时,用于12小时制 + * @see Calendar#HOUR + */ + HOUR(Calendar.HOUR), + /** + * 小时,用于24小时制 + * @see Calendar#HOUR + */ + HOUR_OF_DAY(Calendar.HOUR_OF_DAY), + /** + * 分钟 + * @see Calendar#MINUTE + */ + MINUTE(Calendar.MINUTE), + /** + * 秒 + * @see Calendar#SECOND + */ + SECOND(Calendar.SECOND), + /** + * 毫秒 + * @see Calendar#MILLISECOND + */ + MILLISECOND(Calendar.MILLISECOND); + + // --------------------------------------------------------------- + private int value; + + private DateField(int value) { + this.value = value; + } + + public int getValue() { + return this.value; + } + + /** + * 将 {@link Calendar}相关值转换为DatePart枚举对象
+ * + * @param calendarPartIntValue Calendar中关于Week的int值 + * @return {@link DateField} + */ + public static DateField of(int calendarPartIntValue) { + switch (calendarPartIntValue) { + case Calendar.YEAR: + return YEAR; + case Calendar.MONTH: + return MONTH; + case Calendar.WEEK_OF_YEAR: + return WEEK_OF_YEAR; + case Calendar.WEEK_OF_MONTH: + return WEEK_OF_MONTH; + case Calendar.DAY_OF_MONTH: + return DAY_OF_MONTH; + case Calendar.DAY_OF_YEAR: + return DAY_OF_YEAR; + case Calendar.DAY_OF_WEEK: + return DAY_OF_WEEK; + case Calendar.DAY_OF_WEEK_IN_MONTH: + return DAY_OF_WEEK_IN_MONTH; + case Calendar.MINUTE: + return MINUTE; + case Calendar.SECOND: + return SECOND; + case Calendar.MILLISECOND: + return MILLISECOND; + default: + return null; + } + } +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/date/DatePattern.java b/blade-core-tool/src/main/java/org/springblade/core/tool/date/DatePattern.java new file mode 100644 index 0000000..24d1b76 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/date/DatePattern.java @@ -0,0 +1,71 @@ +package org.springblade.core.tool.date; + + +import org.springblade.core.tool.date.format.FastDateFormat; + +/** + * 日期格式化类,提供常用的日期格式化对象 + * + * @author Looly + * + */ +public class DatePattern { + + //-------------------------------------------------------------------------------------------------------------------------------- Normal + /** 标准日期格式:yyyy-MM-dd */ + public final static String NORM_DATE_PATTERN = "yyyy-MM-dd"; + /** 标准日期格式 {@link FastDateFormat}:yyyy-MM-dd */ + public final static FastDateFormat NORM_DATE_FORMAT = FastDateFormat.getInstance(NORM_DATE_PATTERN); + + /** 标准时间格式:HH:mm:ss */ + public final static String NORM_TIME_PATTERN = "HH:mm:ss"; + /** 标准时间格式 {@link FastDateFormat}:HH:mm:ss */ + public final static FastDateFormat NORM_TIME_FORMAT = FastDateFormat.getInstance(NORM_TIME_PATTERN); + + /** 标准日期时间格式,精确到分:yyyy-MM-dd HH:mm */ + public final static String NORM_DATETIME_MINUTE_PATTERN = "yyyy-MM-dd HH:mm"; + /** 标准日期时间格式,精确到分 {@link FastDateFormat}:yyyy-MM-dd HH:mm */ + public final static FastDateFormat NORM_DATETIME_MINUTE_FORMAT = FastDateFormat.getInstance(NORM_DATETIME_MINUTE_PATTERN); + + /** 标准日期时间格式,精确到秒:yyyy-MM-dd HH:mm:ss */ + public final static String NORM_DATETIME_PATTERN = "yyyy-MM-dd HH:mm:ss"; + /** 标准日期时间格式,精确到秒 {@link FastDateFormat}:yyyy-MM-dd HH:mm:ss */ + public final static FastDateFormat NORM_DATETIME_FORMAT = FastDateFormat.getInstance(NORM_DATETIME_PATTERN); + + /** 标准日期时间格式,精确到毫秒:yyyy-MM-dd HH:mm:ss.SSS */ + public final static String NORM_DATETIME_MS_PATTERN = "yyyy-MM-dd HH:mm:ss.SSS"; + /** 标准日期时间格式,精确到毫秒 {@link FastDateFormat}:yyyy-MM-dd HH:mm:ss.SSS */ + public final static FastDateFormat NORM_DATETIME_MS_FORMAT = FastDateFormat.getInstance(NORM_DATETIME_MS_PATTERN); + + //-------------------------------------------------------------------------------------------------------------------------------- Pure + /** 标准日期格式:yyyyMMdd */ + public final static String PURE_DATE_PATTERN = "yyyyMMdd"; + /** 标准日期格式 {@link FastDateFormat}:yyyyMMdd */ + public final static FastDateFormat PURE_DATE_FORMAT = FastDateFormat.getInstance(PURE_DATE_PATTERN); + + /** 标准日期格式:HHmmss */ + public final static String PURE_TIME_PATTERN = "HHmmss"; + /** 标准日期格式 {@link FastDateFormat}:HHmmss */ + public final static FastDateFormat PURE_TIME_FORMAT = FastDateFormat.getInstance(PURE_TIME_PATTERN); + + /** 标准日期格式:yyyyMMddHHmmss */ + public final static String PURE_DATETIME_PATTERN = "yyyyMMddHHmmss"; + /** 标准日期格式 {@link FastDateFormat}:yyyyMMddHHmmss */ + public final static FastDateFormat PURE_DATETIME_FORMAT = FastDateFormat.getInstance(PURE_DATETIME_PATTERN); + + /** 标准日期格式:yyyyMMddHHmmssSSS */ + public final static String PURE_DATETIME_MS_PATTERN = "yyyyMMddHHmmssSSS"; + /** 标准日期格式 {@link FastDateFormat}:yyyyMMddHHmmssSSS */ + public final static FastDateFormat PURE_DATETIME_MS_FORMAT = FastDateFormat.getInstance(PURE_DATETIME_MS_PATTERN); + + //-------------------------------------------------------------------------------------------------------------------------------- Others + /** HTTP头中日期时间格式:EEE, dd MMM yyyy HH:mm:ss z */ + public final static String HTTP_DATETIME_PATTERN = "EEE, dd MMM yyyy HH:mm:ss z"; + /** HTTP头中日期时间格式 {@link FastDateFormat}:EEE, dd MMM yyyy HH:mm:ss z */ + public final static FastDateFormat HTTP_DATETIME_FORMAT = FastDateFormat.getInstance(HTTP_DATETIME_PATTERN); + + /** HTTP头中日期时间格式:EEE MMM dd HH:mm:ss zzz yyyy */ + public final static String JDK_DATETIME_PATTERN = "EEE MMM dd HH:mm:ss zzz yyyy"; + /** HTTP头中日期时间格式 {@link FastDateFormat}:EEE MMM dd HH:mm:ss zzz yyyy */ + public final static FastDateFormat JDK_DATETIME_FORMAT = FastDateFormat.getInstance(JDK_DATETIME_PATTERN); +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/date/DateTime.java b/blade-core-tool/src/main/java/org/springblade/core/tool/date/DateTime.java new file mode 100644 index 0000000..0190362 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/date/DateTime.java @@ -0,0 +1,753 @@ +package org.springblade.core.tool.date; + +import org.springblade.core.tool.date.format.DateParser; +import org.springblade.core.tool.date.format.DatePrinter; +import org.springblade.core.tool.date.format.FastDateFormat; +import org.springblade.core.tool.utils.Func; +import org.springblade.core.tool.utils.StringUtil; +import org.springframework.util.Assert; + +import java.sql.Timestamp; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.Locale; +import java.util.TimeZone; + + +/** + * 包装java.util.Date + * + * @author xiaoleilu + * + */ +public class DateTime extends Date { + private static final long serialVersionUID = -5395712593979185936L; + + /** 是否可变对象 */ + private boolean mutable = true; + /** 一周的第一天,默认是周一, 在设置或获得 WEEK_OF_MONTH 或 WEEK_OF_YEAR 字段时,Calendar 必须确定一个月或一年的第一个星期,以此作为参考点。 */ + private Week firstDayOfWeek = Week.MONDAY; + + /** + * 转换JDK date为 DateTime + * + * @param date JDK Date + * @return DateTime + */ + public static DateTime of(Date date) { + if(date instanceof DateTime) { + return (DateTime)date; + } + return new DateTime(date); + } + + /** + * 转换 {@link Calendar} 为 DateTime + * + * @param calendar {@link Calendar} + * @return DateTime + */ + public static DateTime of(Calendar calendar) { + return new DateTime(calendar); + } + + /** + * 构造 + * + * @see DatePattern + * @param dateStr Date字符串 + * @param format 格式 + * @return {@link DateTime} + */ + public static DateTime of(String dateStr, String format) { + return new DateTime(dateStr, format); + } + + /** + * 现在的时间 + * + * @return 现在的时间 + */ + public static DateTime now() { + return new DateTime(); + } + + // -------------------------------------------------------------------- Constructor start + /** + * 当前时间 + */ + public DateTime() { + super(); + } + + /** + * 给定日期的构造 + * + * @param date 日期 + */ + public DateTime(Date date) { + this(date.getTime()); + } + + /** + * 给定日期的构造 + * + * @param calendar {@link Calendar} + */ + public DateTime(Calendar calendar) { + this(calendar.getTime()); + } + + /** + * 给定日期毫秒数的构造 + * + * @param timeMillis 日期毫秒数 + */ + public DateTime(long timeMillis) { + super(timeMillis); + } + + /** + * 构造 + * + * @see DatePattern + * @param dateStr Date字符串 + * @param format 格式 + */ + public DateTime(String dateStr, String format) { + this(dateStr, new SimpleDateFormat(format)); + } + + /** + * 构造 + * + * @see DatePattern + * @param dateStr Date字符串 + * @param dateFormat 格式化器 {@link SimpleDateFormat} + */ + public DateTime(String dateStr, DateFormat dateFormat) { + this(parse(dateStr, dateFormat)); + } + + /** + * 构造 + * + * @see DatePattern + * @param dateStr Date字符串 + * @param dateParser 格式化器 {@link DateParser},可以使用 {@link FastDateFormat} + */ + public DateTime(String dateStr, DateParser dateParser) { + this(parse(dateStr, dateParser)); + } + + // -------------------------------------------------------------------- Constructor end + + // -------------------------------------------------------------------- offset start + /** + * 调整日期和时间
+ * 如果此对象为可变对象,返回自身,否则返回新对象,设置是否可变对象见{@link #setMutable(boolean)} + * + * @param datePart 调整的部分 {@link DateField} + * @param offset 偏移量,正数为向后偏移,负数为向前偏移 + * @return 如果此对象为可变对象,返回自身,否则返回新对象 + */ + public DateTime offset(DateField datePart, int offset) { + final Calendar cal = toCalendar(); + cal.add(datePart.getValue(), offset); + + DateTime dt = mutable ? this : Func.clone(this); + return dt.setTimeInternal(cal.getTimeInMillis()); + } + + /** + * 调整日期和时间
+ * 返回调整后的新{@link DateTime},不影响原对象 + * + * @param datePart 调整的部分 {@link DateField} + * @param offset 偏移量,正数为向后偏移,负数为向前偏移 + * @return 如果此对象为可变对象,返回自身,否则返回新对象 + * @since 3.0.9 + */ + public DateTime offsetNew(DateField datePart, int offset) { + final Calendar cal = toCalendar(); + cal.add(datePart.getValue(), offset); + + DateTime dt = Func.clone(this); + return dt.setTimeInternal(cal.getTimeInMillis()); + } + // -------------------------------------------------------------------- offset end + + // -------------------------------------------------------------------- Part of Date start + /** + * 获得日期的某个部分
+ * 例如获得年的部分,则使用 getField(DatePart.YEAR) + * + * @param field 表示日期的哪个部分的枚举 {@link DateField} + * @return 某个部分的值 + */ + public int getField(DateField field) { + return getField(field.getValue()); + } + + /** + * 获得日期的某个部分
+ * 例如获得年的部分,则使用 getField(Calendar.YEAR) + * + * @param field 表示日期的哪个部分的int值 {@link Calendar} + * @return 某个部分的值 + */ + public int getField(int field) { + return toCalendar().get(field); + } + + /** + * 设置日期的某个部分
+ * 如果此对象为可变对象,返回自身,否则返回新对象,设置是否可变对象见{@link #setMutable(boolean)} + * + * @param field 表示日期的哪个部分的枚举 {@link DateField} + * @param value 值 + * @return {@link DateTime} + */ + public DateTime setField(DateField field, int value) { + return setField(field.getValue(), value); + } + + /** + * 设置日期的某个部分
+ * 如果此对象为可变对象,返回自身,否则返回新对象,设置是否可变对象见{@link #setMutable(boolean)} + * + * @param field 表示日期的哪个部分的int值 {@link Calendar} + * @param value 值 + * @return {@link DateTime} + */ + public DateTime setField(int field, int value) { + Calendar calendar = toCalendar(); + calendar.set(field, value); + + DateTime dt = this; + if (false == mutable) { + dt = Func.clone(this); + } + return dt.setTimeInternal(calendar.getTimeInMillis()); + } + + @Override + public void setTime(long time) { + if (mutable) { + super.setTime(time); + } else { + throw new DateException("This is not a mutable object !"); + } + } + + /** + * 获得年的部分 + * + * @return 年的部分 + */ + public int year() { + return getField(DateField.YEAR); + } + + /** + * 获得当前日期所属季度
+ * 1:第一季度
+ * 2:第二季度
+ * 3:第三季度
+ * 4:第四季度
+ * + * @return 第几个季度 + */ + public int season() { + return monthStartFromOne() / 4 + 1; + } + + /** + * 获得当前日期所属季度
+ * + * @return 第几个季度 {@link Season} + */ + public Season seasonEnum() { + return Season.of(season()); + } + + /** + * 获得月份,从0开始计数 + * + * @return 月份 + */ + public int month() { + return getField(DateField.MONTH); + } + + /** + * 获得月份,从1开始计数
+ * 由于{@link Calendar} 中的月份按照0开始计数,导致某些需求容易误解,因此如果想用1表示一月,2表示二月则调用此方法 + * + * @return 月份 + */ + public int monthStartFromOne() { + return month() + 1; + } + + /** + * 获得月份 + * + * @return {@link Month} + */ + public Month monthEnum() { + return Month.of(month()); + } + + /** + * 获得指定日期是所在年份的第几周
+ * 此方法返回值与一周的第一天有关,比如:
+ * 2016年1月3日为周日,如果一周的第一天为周日,那这天是第二周(返回2)
+ * 如果一周的第一天为周一,那这天是第一周(返回1) + * + * @return 周 + * @see #setFirstDayOfWeek(Week) + */ + public int weekOfYear() { + return getField(DateField.WEEK_OF_YEAR); + } + + /** + * 获得指定日期是所在月份的第几周
+ * 此方法返回值与一周的第一天有关,比如:
+ * 2016年1月3日为周日,如果一周的第一天为周日,那这天是第二周(返回2)
+ * 如果一周的第一天为周一,那这天是第一周(返回1) + * + * @return 周 + * @see #setFirstDayOfWeek(Week) + */ + public int weekOfMonth() { + return getField(DateField.WEEK_OF_MONTH); + } + + /** + * 获得指定日期是这个日期所在月份的第几天
+ * + * @return 天 + */ + public int dayOfMonth() { + return getField(DateField.DAY_OF_MONTH); + } + + /** + * 获得指定日期是星期几,1表示周日,2表示周一 + * + * @return 星期几 + */ + public int dayOfWeek() { + return getField(DateField.DAY_OF_WEEK); + } + + /** + * 获得天所在的周是这个月的第几周 + * + * @return 天 + */ + public int dayOfWeekInMonth() { + return getField(DateField.DAY_OF_WEEK_IN_MONTH); + } + + /** + * 获得指定日期是星期几 + * + * @return {@link Week} + */ + public Week dayOfWeekEnum() { + return Week.of(dayOfWeek()); + } + + /** + * 获得指定日期的小时数部分
+ * + * @param is24HourClock 是否24小时制 + * @return 小时数 + */ + public int hour(boolean is24HourClock) { + return getField(is24HourClock ? DateField.HOUR_OF_DAY : DateField.HOUR); + } + + /** + * 获得指定日期的分钟数部分
+ * 例如:10:04:15.250 =》 4 + * + * @return 分钟数 + */ + public int minute() { + return getField(DateField.MINUTE); + } + + /** + * 获得指定日期的秒数部分
+ * + * @return 秒数 + */ + public int second() { + return getField(DateField.SECOND); + } + + /** + * 获得指定日期的毫秒数部分
+ * + * @return 毫秒数 + */ + public int millsecond() { + return getField(DateField.MILLISECOND); + } + + /** + * 是否为上午 + * + * @return 是否为上午 + */ + public boolean isAM() { + return Calendar.AM == getField(DateField.AM_PM); + } + + /** + * 是否为下午 + * + * @return 是否为下午 + */ + public boolean isPM() { + return Calendar.PM == getField(DateField.AM_PM); + } + // -------------------------------------------------------------------- Part of Date end + + /** + * 是否闰年 + * + * @see DateUtil#isLeapYear(int) + * @return 是否闰年 + */ + public boolean isLeapYear() { + return DateUtil.isLeapYear(year()); + } + + /** + * 转换为Calendar,默认{@link TimeZone},默认 {@link Locale} + * + * @return {@link Calendar} + */ + public Calendar toCalendar() { + final Calendar cal = Calendar.getInstance(); + cal.setFirstDayOfWeek(firstDayOfWeek.getValue()); + cal.setTime(this); + return cal; + } + + /** + * 转换为Calendar + * + * @param locale 地域 {@link Locale} + * @return {@link Calendar} + */ + public Calendar toCalendar(Locale locale) { + final Calendar cal = Calendar.getInstance(locale); + cal.setFirstDayOfWeek(firstDayOfWeek.getValue()); + cal.setTime(this); + return cal; + } + + /** + * 转换为Calendar + * + * @param zone 时区 {@link TimeZone} + * @return {@link Calendar} + */ + public Calendar toCalendar(TimeZone zone) { + return toCalendar(zone, Locale.getDefault(Locale.Category.FORMAT)); + } + + /** + * 转换为Calendar + * + * @param zone 时区 {@link TimeZone} + * @param locale 地域 {@link Locale} + * @return {@link Calendar} + */ + public Calendar toCalendar(TimeZone zone, Locale locale) { + final Calendar cal = Calendar.getInstance(zone, locale); + cal.setFirstDayOfWeek(firstDayOfWeek.getValue()); + cal.setTime(this); + return cal; + } + + /** + * 转换为 {@link Date}
+ * 考虑到很多框架(例如Hibernate)的兼容性,提供此方法返回JDK原生的Date对象 + * + * @return {@link Date} + * @since 3.2.2 + */ + public Date toJdkDate() { + return new Date(this.getTime()); + } + + /** + * 转为{@link Timestamp} + * + * @return {@link Timestamp} + */ + public Timestamp toTimestamp() { + return new Timestamp(this.getTime()); + } + + /** + * 转为 {@link java.sql.Date} + * + * @return {@link java.sql.Date} + */ + public java.sql.Date toSqlDate() { + return new java.sql.Date(getTime()); + } + + /** + * 计算相差时长 + * + * @param date 对比的日期 + * @return {@link DateBetween} + */ + public DateBetween between(Date date) { + return new DateBetween(this, date); + } + + /** + * 计算相差时长 + * + * @param date 对比的日期 + * @param unit 单位 {@link DateUnit} + * @return 相差时长 + */ + public long between(Date date, DateUnit unit) { + return new DateBetween(this, date).between(unit); + } + + /** + * 计算相差时长 + * + * @param date 对比的日期 + * @param unit 单位 {@link DateUnit} + * @param formatLevel 格式化级别 + * @return 相差时长 + */ + public String between(Date date, DateUnit unit, BetweenFormater.Level formatLevel) { + return new DateBetween(this, date).toString(formatLevel); + } + + /** + * 当前日期是否在日期指定范围内
+ * 起始日期和结束日期可以互换 + * + * @param beginDate 起始日期 + * @param endDate 结束日期 + * @return 是否在范围内 + * @since 3.0.8 + */ + public boolean isIn(Date beginDate, Date endDate) { + long beginMills = beginDate.getTime(); + long endMills = endDate.getTime(); + long thisMills = this.getTime(); + + return thisMills >= Math.min(beginMills, endMills) && thisMills <= Math.max(beginMills, endMills); + } + + /** + * 是否在给定日期之前或与给定日期相等 + * + * @param date 日期 + * @return 是否在给定日期之前或与给定日期相等 + * @since 3.0.9 + */ + public boolean isBeforeOrEquals(Date date) { + if (null == date) { + throw new NullPointerException("Date to compare is null !"); + } + return compareTo(date) <= 0; + } + + /** + * 是否在给定日期之后或与给定日期相等 + * + * @param date 日期 + * @return 是否在给定日期之后或与给定日期相等 + * @since 3.0.9 + */ + public boolean isAfterOrEquals(Date date) { + if (null == date) { + throw new NullPointerException("Date to compare is null !"); + } + return compareTo(date) >= 0; + } + + /** + * 对象是否可变
+ * 如果为不可变对象,以下方法将返回新方法: + *

+ * 如果为不可变对象,{@link DateTime#setTime(long)}将抛出异常 + * + * @return 对象是否可变 + */ + public boolean isMutable() { + return mutable; + } + + /** + * 设置对象是否可变 如果为不可变对象,以下方法将返回新方法: + * + * 如果为不可变对象,{@link DateTime#setTime(long)}将抛出异常 + * + * @param mutable 是否可变 + * @return this + */ + public DateTime setMutable(boolean mutable) { + this.mutable = mutable; + return this; + } + + /** + * 获得一周的第一天,默认为周一 + * + * @return 一周的第一天 + */ + public Week getFirstDayOfWeek() { + return firstDayOfWeek; + } + + /** + * 设置一周的第一天
+ * JDK的Calendar中默认一周的第一天是周日,Hutool中将此默认值设置为周一
+ * 设置一周的第一天主要影响{@link #weekOfMonth()}和{@link #weekOfYear()} 两个方法 + * + * @param firstDayOfWeek 一周的第一天 + * @return this + * @see #weekOfMonth() + * @see #weekOfYear() + */ + public DateTime setFirstDayOfWeek(Week firstDayOfWeek) { + this.firstDayOfWeek = firstDayOfWeek; + return this; + } + + // -------------------------------------------------------------------- toString start + /** + * 转为"yyyy-MM-dd yyyy-MM-dd HH:mm:ss " 格式字符串 + * + * @return "yyyy-MM-dd yyyy-MM-dd HH:mm:ss " 格式字符串 + */ + @Override + public String toString() { + return toString(DatePattern.NORM_DATETIME_FORMAT); + } + + /** + * 转为"yyyy-MM-dd " 格式字符串 + * + * @return "yyyy-MM-dd " 格式字符串 + * @since 4.0.0 + */ + public String toDateStr() { + return toString(DatePattern.NORM_DATE_PATTERN); + } + + /** + * 转为字符串 + * + * @param format 日期格式,常用格式见: {@link DatePattern} + * @return String + */ + public String toString(String format) { + return toString(FastDateFormat.getInstance(format)); + } + + /** + * 转为字符串 + * + * @param format {@link DatePrinter} 或 {@link FastDateFormat} + * @return String + */ + public String toString(DatePrinter format) { + return format.format(this); + } + + /** + * 转为字符串 + * + * @param format {@link SimpleDateFormat} + * @return String + */ + public String toString(DateFormat format) { + return format.format(this); + } + + /** + * @return 输出精确到毫秒的标准日期形式 + */ + public String toMsStr() { + return toString(DatePattern.NORM_DATETIME_MS_FORMAT); + } + // -------------------------------------------------------------------- toString end + + /** + * 转换字符串为Date + * + * @param dateStr 日期字符串 + * @param dateFormat {@link SimpleDateFormat} + * @return {@link Date} + */ + private static Date parse(String dateStr, DateFormat dateFormat) { + try { + return dateFormat.parse(dateStr); + } catch (Exception e) { + String pattern; + if (dateFormat instanceof SimpleDateFormat) { + pattern = ((SimpleDateFormat) dateFormat).toPattern(); + } else { + pattern = dateFormat.toString(); + } + throw new DateException(StringUtil.format("Parse [{}] with format [{}] error!", dateStr, pattern), e); + } + } + + /** + * 转换字符串为Date + * + * @param dateStr 日期字符串 + * @param parser {@link FastDateFormat} + * @return {@link Date} + */ + private static Date parse(String dateStr, DateParser parser) { + Assert.notNull(parser, "Parser or DateFromat must be not null !"); + //Assert.notBlank(dateStr, "Date String must be not blank !"); + try { + return parser.parse(dateStr); + } catch (Exception e) { + throw new DateException("Parse [{}] with format [{}] error!", dateStr, parser.getPattern(), e); + } + } + + /** + * 设置日期时间 + * + * @param time 日期时间毫秒 + * @return this + */ + private DateTime setTimeInternal(long time) { + super.setTime(time); + return this; + } +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/date/DateUnit.java b/blade-core-tool/src/main/java/org/springblade/core/tool/date/DateUnit.java new file mode 100644 index 0000000..1fd507e --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/date/DateUnit.java @@ -0,0 +1,33 @@ +package org.springblade.core.tool.date; + +/** + * 日期时间单位,每个单位都是以毫秒为基数 + * @author Looly + * + */ +public enum DateUnit { + /** 一毫秒 */ + MS(1), + /** 一秒的毫秒数 */ + SECOND(1000), + /**一分钟的毫秒数 */ + MINUTE(SECOND.getMillis() * 60), + /**一小时的毫秒数 */ + HOUR(MINUTE.getMillis() * 60), + /**一天的毫秒数 */ + DAY(HOUR.getMillis() * 24), + /**一周的毫秒数 */ + WEEK(DAY.getMillis() * 7); + + private long millis; + DateUnit(long millis){ + this.millis = millis; + } + + /** + * @return 单位对应的毫秒数 + */ + public long getMillis(){ + return this.millis; + } +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/date/DateUtil.java b/blade-core-tool/src/main/java/org/springblade/core/tool/date/DateUtil.java new file mode 100644 index 0000000..b37d6c4 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/date/DateUtil.java @@ -0,0 +1,1471 @@ +package org.springblade.core.tool.date; + +import org.springblade.core.tool.date.format.DateParser; +import org.springblade.core.tool.date.format.DatePrinter; +import org.springblade.core.tool.date.format.FastDateFormat; +import org.springblade.core.tool.utils.Func; +import org.springblade.core.tool.utils.Exceptions; +import org.springblade.core.tool.utils.StringPool; +import org.springblade.core.tool.utils.StringUtil; + +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.*; + +/** + * 时间工具类 + * + * @author xiaoleilu + */ +public class DateUtil { + + /** + * 转换为{@link DateTime}对象 + * + * @return 当前时间 + */ + public static DateTime date() { + return new DateTime(); + } + + /** + * {@link Date}类型时间转为{@link DateTime} + * + * @param date Long类型Date(Unix时间戳) + * @return 时间对象 + * @since 3.0.7 + */ + public static DateTime date(Date date) { + if (date instanceof DateTime) { + return (DateTime) date; + } + return new DateTime(date); + } + + /** + * Long类型时间转为{@link DateTime}
+ * 同时支持10位秒级别时间戳和13位毫秒级别时间戳 + * + * @param date Long类型Date(Unix时间戳) + * @return 时间对象 + */ + public static DateTime date(long date) { + return new DateTime(date); + } + + /** + * {@link Calendar}类型时间转为{@link DateTime} + * + * @param calendar {@link Calendar} + * @return 时间对象 + */ + public static DateTime date(Calendar calendar) { + return new DateTime(calendar); + } + + /** + * 转换为Calendar对象 + * + * @param date 日期对象 + * @return Calendar对象 + */ + public static Calendar calendar(Date date) { + return calendar(date.getTime()); + } + + /** + * 转换为Calendar对象 + * + * @param millis 时间戳 + * @return Calendar对象 + */ + public static Calendar calendar(long millis) { + final Calendar cal = Calendar.getInstance(); + cal.setTimeInMillis(millis); + return cal; + } + + /** + * 当前时间,格式 yyyy-MM-dd HH:mm:ss + * + * @return 当前时间的标准形式字符串 + */ + public static String now() { + return formatDateTime(new DateTime()); + } + + /** + * 当前时间long + * + * @param isNano 是否为高精度时间 + * @return 时间 + */ + public static long current(boolean isNano) { + return isNano ? System.nanoTime() : System.currentTimeMillis(); + } + + /** + * 当前时间秒数 + * + * @return 当前时间秒数 + * @since 4.0.0 + */ + public static long currentSeconds() { + return System.currentTimeMillis() / 1000; + } + + /** + * 当前日期,格式 yyyy-MM-dd + * + * @return 当前日期的标准形式字符串 + */ + public static String today() { + return formatDate(new DateTime()); + } + + // -------------------------------------------------------------- Part of Date start + /** + * 获得年的部分 + * + * @param date 日期 + * @return 年的部分 + */ + public static int year(Date date) { + return DateTime.of(date).year(); + } + + /** + * 获得指定日期所属季节 + * + * @param date 日期 + * @return 第几个季节 + */ + public static int season(Date date) { + return DateTime.of(date).season(); + } + + /** + * 获得月份,从0开始计数 + * + * @param date 日期 + * @return 月份,从0开始计数 + */ + public static int month(Date date) { + return DateTime.of(date).month(); + } + + /** + * 获得月份 + * + * @param date 日期 + * @return {@link Month} + */ + public static Month monthEnum(Date date) { + return DateTime.of(date).monthEnum(); + } + + /** + * 获得指定日期是所在年份的第几周
+ * + * @param date 日期 + * @return 周 + */ + public static int weekOfYear(Date date) { + return DateTime.of(date).weekOfYear(); + } + + /** + * 获得指定日期是所在月份的第几周
+ * + * @param date 日期 + * @return 周 + */ + public static int weekOfMonth(Date date) { + return DateTime.of(date).weekOfMonth(); + } + + /** + * 获得指定日期是这个日期所在月份的第几天
+ * + * @param date 日期 + * @return 天 + */ + public static int dayOfMonth(Date date) { + return DateTime.of(date).dayOfMonth(); + } + + /** + * 获得指定日期是星期几,1表示周日,2表示周一 + * + * @param date 日期 + * @return 天 + */ + public static int dayOfWeek(Date date) { + return DateTime.of(date).dayOfWeek(); + } + + /** + * 获得指定日期是星期几 + * + * @param date 日期 + * @return {@link Week} + */ + public static Week dayOfWeekEnum(Date date) { + return DateTime.of(date).dayOfWeekEnum(); + } + + /** + * 获得指定日期的小时数部分
+ * + * @param date 日期 + * @param is24HourClock 是否24小时制 + * @return 小时数 + */ + public static int hour(Date date, boolean is24HourClock) { + return DateTime.of(date).hour(is24HourClock); + } + + /** + * 获得指定日期的分钟数部分
+ * 例如:10:04:15.250 =》 4 + * + * @param date 日期 + * @return 分钟数 + */ + public static int minute(Date date) { + return DateTime.of(date).minute(); + } + + /** + * 获得指定日期的秒数部分
+ * + * @param date 日期 + * @return 秒数 + */ + public static int second(Date date) { + return DateTime.of(date).second(); + } + + /** + * 获得指定日期的毫秒数部分
+ * + * @param date 日期 + * @return 毫秒数 + */ + public static int millsecond(Date date) { + return DateTime.of(date).millsecond(); + } + + /** + * 是否为上午 + * + * @param date 日期 + * @return 是否为上午 + */ + public static boolean isAM(Date date) { + return DateTime.of(date).isAM(); + } + + /** + * 是否为下午 + * + * @param date 日期 + * @return 是否为下午 + */ + public static boolean isPM(Date date) { + return DateTime.of(date).isPM(); + } + + /** + * @return 今年 + */ + public static int thisYear() { + return year(date()); + } + + /** + * @return 当前月份 + */ + public static int thisMonth() { + return month(date()); + } + + /** + * @return 当前月份 {@link Month} + */ + public static Month thisMonthEnum() { + return monthEnum(date()); + } + + /** + * @return 当前日期所在年份的第几周 + */ + public static int thisWeekOfYear() { + return weekOfYear(date()); + } + + /** + * @return 当前日期所在年份的第几周 + */ + public static int thisWeekOfMonth() { + return weekOfMonth(date()); + } + + /** + * @return 当前日期是这个日期所在月份的第几天 + */ + public static int thisDayOfMonth() { + return dayOfMonth(date()); + } + + /** + * @return 当前日期是星期几 + */ + public static int thisDayOfWeek() { + return dayOfWeek(date()); + } + + /** + * @return 当前日期是星期几 {@link Week} + */ + public static Week thisDayOfWeekEnum() { + return dayOfWeekEnum(date()); + } + + /** + * @param is24HourClock 是否24小时制 + * @return 当前日期的小时数部分
+ */ + public static int thisHour(boolean is24HourClock) { + return hour(date(), is24HourClock); + } + + /** + * @return 当前日期的分钟数部分
+ */ + public static int thisMinute() { + return minute(date()); + } + + /** + * @return 当前日期的秒数部分
+ */ + public static int thisSecond() { + return second(date()); + } + + /** + * @return 当前日期的毫秒数部分
+ */ + public static int thisMillsecond() { + return millsecond(date()); + } + // -------------------------------------------------------------- Part of Date end + + /** + * 获得指定日期年份和季节
+ * 格式:[20131]表示2013年第一季度 + * + * @param date 日期 + * @return Season ,类似于 20132 + */ + public static String yearAndSeason(Date date) { + return yearAndSeason(calendar(date)); + } + + /** + * 获得指定日期区间内的年份和季节
+ * + * @param startDate 起始日期(包含) + * @param endDate 结束日期(包含) + * @return Season列表 ,元素类似于 20132 + */ + public static LinkedHashSet yearAndSeasons(Date startDate, Date endDate) { + final LinkedHashSet seasons = new LinkedHashSet(); + if (startDate == null || endDate == null) { + return seasons; + } + + final Calendar cal = Calendar.getInstance(); + cal.setTime(startDate); + while (true) { + // 如果开始时间超出结束时间,让结束时间为开始时间,处理完后结束循环 + if (startDate.after(endDate)) { + startDate = endDate; + } + + seasons.add(yearAndSeason(cal)); + + if (startDate.equals(endDate)) { + break; + } + + cal.add(Calendar.MONTH, 3); + startDate = cal.getTime(); + } + + return seasons; + } + + // ------------------------------------ Format start ---------------------------------------------- + /** + * 根据特定格式格式化日期 + * + * @param date 被格式化的日期 + * @param format 日期格式,常用格式见: {@link DatePattern} + * @return 格式化后的字符串 + */ + public static String format(Date date, String format) { + if(null == date || StringUtil.isBlank(format)) { + return null; + } + return format(date, FastDateFormat.getInstance(format)); + } + + /** + * 根据特定格式格式化日期 + * + * @param date 被格式化的日期 + * @param format {@link DatePrinter} 或 {@link FastDateFormat} + * @return 格式化后的字符串 + */ + public static String format(Date date, DatePrinter format) { + if(null == format || null == date) { + return null; + } + return format.format(date); + } + + /** + * 根据特定格式格式化日期 + * + * @param date 被格式化的日期 + * @param format {@link SimpleDateFormat} + * @return 格式化后的字符串 + */ + public static String format(Date date, DateFormat format) { + if(null == format || null == date) { + return null; + } + return format.format(date); + } + + /** + * 格式化日期时间
+ * 格式 yyyy-MM-dd HH:mm:ss + * + * @param date 被格式化的日期 + * @return 格式化后的日期 + */ + public static String formatDateTime(Date date) { + if (null == date) { + return null; + } + return DatePattern.NORM_DATETIME_FORMAT.format(date); + } + + /** + * 格式化日期部分(不包括时间)
+ * 格式 yyyy-MM-dd + * + * @param date 被格式化的日期 + * @return 格式化后的字符串 + */ + public static String formatDate(Date date) { + if (null == date) { + return null; + } + return DatePattern.NORM_DATE_FORMAT.format(date); + } + + /** + * 格式化时间
+ * 格式 HH:mm:ss + * + * @param date 被格式化的日期 + * @return 格式化后的字符串 + * @since 3.0.1 + */ + public static String formatTime(Date date) { + if (null == date) { + return null; + } + return DatePattern.NORM_TIME_FORMAT.format(date); + } + + /** + * 格式化为Http的标准日期格式 + * + * @param date 被格式化的日期 + * @return HTTP标准形式日期字符串 + */ + public static String formatHttpDate(Date date) { + if (null == date) { + return null; + } + return DatePattern.HTTP_DATETIME_FORMAT.format(date); + } + // ------------------------------------ Format end ---------------------------------------------- + + // ------------------------------------ Parse start ---------------------------------------------- + + /** + * 构建DateTime对象 + * + * @param dateStr Date字符串 + * @param dateFormat 格式化器 {@link SimpleDateFormat} + * @return DateTime对象 + */ + public static DateTime parse(String dateStr, DateFormat dateFormat) { + return new DateTime(dateStr, dateFormat); + } + + /** + * 构建DateTime对象 + * + * @param dateStr Date字符串 + * @param parser 格式化器,{@link FastDateFormat} + * @return DateTime对象 + */ + public static DateTime parse(String dateStr, DateParser parser) { + return new DateTime(dateStr, parser); + } + + /** + * 将特定格式的日期转换为Date对象 + * + * @param dateStr 特定格式的日期 + * @param format 格式,例如yyyy-MM-dd + * @return 日期对象 + */ + public static DateTime parse(String dateStr, String format) { + return new DateTime(dateStr, format); + } + + /** + * 将字符串转换为时间 + * + * @param dateStr 时间字符串 + * @param format ConcurrentDateFormat + * @return 时间 + */ + public static Date parse(String dateStr, ConcurrentDateFormat format) { + try { + return format.parse(dateStr); + } catch (ParseException e) { + throw Exceptions.unchecked(e); + } + } + + /** + * 格式yyyy-MM-dd HH:mm:ss + * + * @param dateString 标准形式的时间字符串 + * @return 日期对象 + */ + public static DateTime parseDateTime(String dateString) { + dateString = normalize(dateString); + return parse(dateString, DatePattern.NORM_DATETIME_FORMAT); + } + + /** + * 格式yyyy-MM-dd + * + * @param dateString 标准形式的日期字符串 + * @return 日期对象 + */ + public static DateTime parseDate(String dateString) { + dateString = normalize(dateString); + return parse(dateString, DatePattern.NORM_DATE_FORMAT); + } + + /** + * 解析时间,格式HH:mm:ss,默认为1970-01-01 + * + * @param timeString 标准形式的日期字符串 + * @return 日期对象 + */ + public static DateTime parseTime(String timeString) { + timeString = normalize(timeString); + return parse(timeString, DatePattern.NORM_TIME_FORMAT); + } + + /** + * 解析时间,格式HH:mm:ss,日期默认为今天 + * + * @param timeString 标准形式的日期字符串 + * @return 日期对象 + * @since 3.1.1 + */ + public static DateTime parseTimeToday(String timeString) { + timeString = StringUtil.format("{} {}", today(), timeString); + return parse(timeString, DatePattern.NORM_TIME_FORMAT); + } + + /** + * 将日期字符串转换为{@link DateTime}对象,格式:
+ *
    + *
  1. yyyy-MM-dd HH:mm:ss
  2. + *
  3. yyyy/MM/dd HH:mm:ss
  4. + *
  5. yyyy.MM.dd HH:mm:ss
  6. + *
  7. yyyy年MM月dd日 HH时mm分ss秒
  8. + *
  9. yyyy-MM-dd
  10. + *
  11. yyyy/MM/dd
  12. + *
  13. yyyy.MM.dd
  14. + *
  15. HH:mm:ss
  16. + *
  17. HH时mm分ss秒
  18. + *
  19. yyyy-MM-dd HH:mm
  20. + *
  21. yyyy-MM-dd HH:mm:ss.SSS
  22. + *
  23. yyyyMMddHHmmss
  24. + *
  25. yyyyMMddHHmmssSSS
  26. + *
  27. yyyyMMdd
  28. + *
+ * + * @param dateStr 日期字符串 + * @return 日期 + */ + public static DateTime parse(String dateStr) { + if (null == dateStr) { + return null; + } + //去掉两边空格并去掉中文日期中的“日”,以规范长度 + dateStr = dateStr.trim().replace("日", ""); + int length = dateStr.length(); + + if(Func.isNumeric(dateStr)) { + //纯数字形式 + if(length == DatePattern.PURE_DATETIME_PATTERN.length()) { + return parse(dateStr, DatePattern.PURE_DATETIME_FORMAT); + } else if(length == DatePattern.PURE_DATETIME_MS_PATTERN.length()) { + return parse(dateStr, DatePattern.PURE_DATETIME_MS_FORMAT); + } else if(length == DatePattern.PURE_DATE_PATTERN.length()) { + return parse(dateStr, DatePattern.PURE_DATE_FORMAT); + } else if(length == DatePattern.PURE_TIME_PATTERN.length()) { + return parse(dateStr, DatePattern.PURE_TIME_FORMAT); + } + } + + if (length == DatePattern.NORM_DATETIME_PATTERN.length() || length == DatePattern.NORM_DATETIME_PATTERN.length()+1) { + return parseDateTime(dateStr); + } else if (length == DatePattern.NORM_DATE_PATTERN.length()) { + return parseDate(dateStr); + } else if (length == DatePattern.NORM_TIME_PATTERN.length() || length == DatePattern.NORM_TIME_PATTERN.length()+1) { + return parseTime(dateStr); + } else if (length == DatePattern.NORM_DATETIME_MINUTE_PATTERN.length() || length == DatePattern.NORM_DATETIME_MINUTE_PATTERN.length()+1) { + return parse(normalize(dateStr), DatePattern.NORM_DATETIME_MINUTE_PATTERN); + } else if (length >= DatePattern.NORM_DATETIME_MS_PATTERN.length() - 2) { + return parse(normalize(dateStr), DatePattern.NORM_DATETIME_MS_PATTERN); + } + + // 没有更多匹配的时间格式 + throw new DateException("No format fit for date String [{}] !", dateStr); + } + + // ------------------------------------ Parse end ---------------------------------------------- + + // ------------------------------------ Offset start ---------------------------------------------- + /** + * 获取某天的开始时间 + * + * @param date 日期 + * @return {@link DateTime} + */ + public static DateTime beginOfDay(Date date) { + return new DateTime(beginOfDay(calendar(date))); + } + + /** + * 获取某天的结束时间 + * + * @param date 日期 + * @return {@link DateTime} + */ + public static DateTime endOfDay(Date date) { + return new DateTime(endOfDay(calendar(date))); + } + + /** + * 获取某天的开始时间 + * + * @param calendar 日期 {@link Calendar} + * @return {@link Calendar} + */ + public static Calendar beginOfDay(Calendar calendar) { + calendar.set(Calendar.HOUR_OF_DAY, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.SECOND, 0); + calendar.set(Calendar.MILLISECOND, 0); + return calendar; + } + + /** + * 获取某天的结束时间 + * + * @param calendar 日期 {@link Calendar} + * @return {@link Calendar} + */ + public static Calendar endOfDay(Calendar calendar) { + calendar.set(Calendar.HOUR_OF_DAY, 23); + calendar.set(Calendar.MINUTE, 59); + calendar.set(Calendar.SECOND, 59); + calendar.set(Calendar.MILLISECOND, 999); + return calendar; + } + + /** + * 获取某周的开始时间 + * + * @param date 日期 + * @return {@link DateTime} + */ + public static DateTime beginOfWeek(Date date) { + return new DateTime(beginOfWeek(calendar(date))); + } + + /** + * 获取某周的结束时间 + * + * @param date 日期 + * @return {@link DateTime} + */ + public static DateTime endOfWeek(Date date) { + return new DateTime(endOfWeek(calendar(date))); + } + + /** + * 获取某周的开始时间 + * + * @param calendar 日期 {@link Calendar} + * @return {@link Calendar} + */ + public static Calendar beginOfWeek(Calendar calendar) { + return beginOfWeek(calendar, true); + } + + /** + * 获取某周的开始时间,周一定为一周的开始时间 + * + * @param calendar 日期 {@link Calendar} + * @param isMondayAsFirstDay 是否周一做为一周的第一天(false表示周日做为第一天) + * @return {@link Calendar} + * @since 3.1.2 + */ + public static Calendar beginOfWeek(Calendar calendar, boolean isMondayAsFirstDay) { + if(isMondayAsFirstDay) { + //设置周一为一周开始 + calendar.setFirstDayOfWeek(Week.MONDAY.getValue()); + calendar.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY); + }else { + calendar.set(Calendar.DAY_OF_WEEK, Calendar.SUNDAY); + } + return beginOfDay(calendar); + } + + /** + * 获取某周的结束时间,周日定为一周的结束 + * + * @param calendar 日期 {@link Calendar} + * @return {@link Calendar} + */ + public static Calendar endOfWeek(Calendar calendar) { + return endOfWeek(calendar, true); + } + + /** + * 获取某周的结束时间 + * + * @param calendar 日期 {@link Calendar} + * @param isSundayAsLastDay 是否周日做为一周的最后一天(false表示周六做为最后一天) + * @return {@link Calendar} + * @since 3.1.2 + */ + public static Calendar endOfWeek(Calendar calendar, boolean isSundayAsLastDay) { + if(isSundayAsLastDay) { + //设置周一为一周开始 + calendar.setFirstDayOfWeek(Week.MONDAY.getValue()); + calendar.set(Calendar.DAY_OF_WEEK, Calendar.SUNDAY); + }else { + calendar.set(Calendar.DAY_OF_WEEK, Calendar.SATURDAY); + } + return endOfDay(calendar); + } + + /** + * 获取某月的开始时间 + * + * @param date 日期 + * @return {@link DateTime} + */ + public static DateTime beginOfMonth(Date date) { + return new DateTime(beginOfMonth(calendar(date))); + } + + /** + * 获取某月的结束时间 + * + * @param date 日期 + * @return {@link DateTime} + */ + public static DateTime endOfMonth(Date date) { + return new DateTime(endOfMonth(calendar(date))); + } + + /** + * 获取某月的开始时间 + * + * @param calendar 日期 {@link Calendar} + * @return {@link Calendar} + */ + public static Calendar beginOfMonth(Calendar calendar) { + calendar.set(Calendar.DAY_OF_MONTH, 1); + return beginOfDay(calendar); + } + + /** + * 获取某月的结束时间 + * + * @param calendar 日期 {@link Calendar} + * @return {@link Calendar} + */ + public static Calendar endOfMonth(Calendar calendar) { + calendar.set(Calendar.DAY_OF_MONTH, calendar.getActualMaximum(Calendar.DAY_OF_MONTH)); + return endOfDay(calendar); + } + + /** + * 获取某年的开始时间 + * + * @param date 日期 + * @return {@link DateTime} + */ + public static DateTime beginOfYear(Date date) { + return new DateTime(beginOfYear(calendar(date))); + } + + /** + * 获取某年的结束时间 + * + * @param date 日期 + * @return {@link DateTime} + */ + public static DateTime endOfYear(Date date) { + return new DateTime(endOfYear(calendar(date))); + } + + /** + * 获取某年的开始时间 + * + * @param calendar 日期 {@link Calendar} + * @return {@link Calendar} + */ + public static Calendar beginOfYear(Calendar calendar) { + calendar.set(Calendar.MONTH, Calendar.JANUARY); + return beginOfMonth(calendar); + } + + /** + * 获取某年的结束时间 + * + * @param calendar 日期 {@link Calendar} + * @return {@link Calendar} + */ + public static Calendar endOfYear(Calendar calendar) { + calendar.set(Calendar.MONTH, Calendar.DECEMBER); + return endOfMonth(calendar); + } + + // --------------------------------------------------- Offset for now + /** + * 昨天 + * + * @return 昨天 + */ + public static DateTime yesterday() { + return offsetDay(new DateTime(), -1); + } + + /** + * 明天 + * + * @return 明天 + * @since 3.0.1 + */ + public static DateTime tomorrow() { + return offsetDay(new DateTime(), 1); + } + + /** + * 上周 + * + * @return 上周 + */ + public static DateTime lastWeek() { + return offsetWeek(new DateTime(), -1); + } + + /** + * 下周 + * + * @return 下周 + * @since 3.0.1 + */ + public static DateTime nextWeek() { + return offsetWeek(new DateTime(), 1); + } + + /** + * 上个月 + * + * @return 上个月 + */ + public static DateTime lastMonth() { + return offsetMonth(new DateTime(), -1); + } + + /** + * 下个月 + * + * @return 下个月 + * @since 3.0.1 + */ + public static DateTime nextMonth() { + return offsetMonth(new DateTime(), 1); + } + + /** + * 偏移毫秒数 + * + * @param date 日期 + * @param offset 偏移毫秒数,正数向未来偏移,负数向历史偏移 + * @return 偏移后的日期 + */ + public static DateTime offsetMillisecond(Date date, int offset) { + return offset(date, DateField.MILLISECOND, offset); + } + + /** + * 偏移秒数 + * + * @param date 日期 + * @param offset 偏移秒数,正数向未来偏移,负数向历史偏移 + * @return 偏移后的日期 + */ + public static DateTime offsetSecond(Date date, int offset) { + return offset(date, DateField.SECOND, offset); + } + + /** + * 偏移分钟 + * + * @param date 日期 + * @param offset 偏移分钟数,正数向未来偏移,负数向历史偏移 + * @return 偏移后的日期 + */ + public static DateTime offsetMinute(Date date, int offset) { + return offset(date, DateField.MINUTE, offset); + } + + /** + * 偏移小时 + * + * @param date 日期 + * @param offset 偏移小时数,正数向未来偏移,负数向历史偏移 + * @return 偏移后的日期 + */ + public static DateTime offsetHour(Date date, int offset) { + return offset(date, DateField.HOUR_OF_DAY, offset); + } + + /** + * 偏移天 + * + * @param date 日期 + * @param offset 偏移天数,正数向未来偏移,负数向历史偏移 + * @return 偏移后的日期 + */ + public static DateTime offsetDay(Date date, int offset) { + return offset(date, DateField.DAY_OF_YEAR, offset); + } + + /** + * 偏移周 + * + * @param date 日期 + * @param offset 偏移周数,正数向未来偏移,负数向历史偏移 + * @return 偏移后的日期 + */ + public static DateTime offsetWeek(Date date, int offset) { + return offset(date, DateField.WEEK_OF_YEAR, offset); + } + + /** + * 偏移月 + * + * @param date 日期 + * @param offset 偏移月数,正数向未来偏移,负数向历史偏移 + * @return 偏移后的日期 + */ + public static DateTime offsetMonth(Date date, int offset) { + return offset(date, DateField.MONTH, offset); + } + + /** + * 获取指定日期偏移指定时间后的时间 + * + * @param date 基准日期 + * @param dateField 偏移的粒度大小(小时、天、月等){@link DateField} + * @param offset 偏移量,正数为向后偏移,负数为向前偏移 + * @return 偏移后的日期 + */ + public static DateTime offset(Date date, DateField dateField, int offset) { + Calendar cal = Calendar.getInstance(); + cal.setTime(date); + cal.add(dateField.getValue(), offset); + return new DateTime(cal.getTime()); + } + + /** + * 获取指定日期偏移指定时间后的时间 + * + * @param date 基准日期 + * @param dateField 偏移的粒度大小(小时、天、月等){@link DateField} + * @param offset 偏移量,正数为向后偏移,负数为向前偏移 + * @return 偏移后的日期 + * @deprecated please use {@link DateUtil#offset(Date, DateField, int)} + */ + @Deprecated + public static DateTime offsetDate(Date date, DateField dateField, int offset) { + return offset(date, dateField, offset); + } + // ------------------------------------ Offset end ---------------------------------------------- + + /** + * 判断两个日期相差的时长,只保留绝对值 + * + * @param beginDate 起始日期 + * @param endDate 结束日期 + * @param unit 相差的单位:相差 天{@link DateUnit#DAY}、小时{@link DateUnit#HOUR} 等 + * @return 日期差 + */ + public static long between(Date beginDate, Date endDate, DateUnit unit) { + return between(beginDate, endDate, unit, true); + } + + /** + * 判断两个日期相差的时长 + * + * @param beginDate 起始日期 + * @param endDate 结束日期 + * @param unit 相差的单位:相差 天{@link DateUnit#DAY}、小时{@link DateUnit#HOUR} 等 + * @param isAbs 日期间隔是否只保留绝对值正数 + * @return 日期差 + * @since 3.3.1 + */ + public static long between(Date beginDate, Date endDate, DateUnit unit, boolean isAbs) { + return new DateBetween(beginDate, endDate, isAbs).between(unit); + } + + /** + * 判断两个日期相差的毫秒数 + * + * @param beginDate 起始日期 + * @param endDate 结束日期 + * @return 日期差 + * @since 3.0.1 + */ + public static long betweenMs(Date beginDate, Date endDate) { + return new DateBetween(beginDate, endDate).between(DateUnit.MS); + } + + /** + * 判断两个日期相差的天数
+ * + *
+	 * 有时候我们计算相差天数的时候需要忽略时分秒。
+	 * 比如:2016-02-01 23:59:59和2016-02-02 00:00:00相差一秒
+	 * 如果isReset为false相差天数为0。
+	 * 如果isReset为true相差天数将被计算为1
+	 * 
+ * + * @param beginDate 起始日期 + * @param endDate 结束日期 + * @param isReset 是否重置时间为起始时间 + * @return 日期差 + * @since 3.0.1 + */ + public static long betweenDay(Date beginDate, Date endDate, boolean isReset) { + if (isReset) { + beginDate = beginOfDay(beginDate); + endDate = beginOfDay(endDate); + } + return between(beginDate, endDate, DateUnit.DAY); + } + + /** + * 计算两个日期相差月数
+ * 在非重置情况下,如果起始日期的天小于结束日期的天,月数要少算1(不足1个月) + * + * @param beginDate 起始日期 + * @param endDate 结束日期 + * @param isReset 是否重置时间为起始时间(重置天时分秒) + * @return 相差月数 + * @since 3.0.8 + */ + public static long betweenMonth(Date beginDate, Date endDate, boolean isReset) { + return new DateBetween(beginDate, endDate).betweenMonth(isReset); + } + + /** + * 计算两个日期相差年数
+ * 在非重置情况下,如果起始日期的月小于结束日期的月,年数要少算1(不足1年) + * + * @param beginDate 起始日期 + * @param endDate 结束日期 + * @param isReset 是否重置时间为起始时间(重置月天时分秒) + * @return 相差年数 + * @since 3.0.8 + */ + public static long betweenYear(Date beginDate, Date endDate, boolean isReset) { + return new DateBetween(beginDate, endDate).betweenYear(isReset); + } + + /** + * 格式化日期间隔输出 + * + * @param beginDate 起始日期 + * @param endDate 结束日期 + * @param level 级别,按照天、小时、分、秒、毫秒分为5个等级 + * @return XX天XX小时XX分XX秒 + */ + public static String formatBetween(Date beginDate, Date endDate, BetweenFormater.Level level) { + return formatBetween(between(beginDate, endDate, DateUnit.MS), level); + } + + /** + * 格式化日期间隔输出,精确到毫秒 + * + * @param beginDate 起始日期 + * @param endDate 结束日期 + * @return XX天XX小时XX分XX秒 + * @since 3.0.1 + */ + public static String formatBetween(Date beginDate, Date endDate) { + return formatBetween(between(beginDate, endDate, DateUnit.MS)); + } + + /** + * 格式化日期间隔输出 + * + * @param betweenMs 日期间隔 + * @param level 级别,按照天、小时、分、秒、毫秒分为5个等级 + * @return XX天XX小时XX分XX秒XX毫秒 + */ + public static String formatBetween(long betweenMs, BetweenFormater.Level level) { + return new BetweenFormater(betweenMs, level).format(); + } + + /** + * 格式化日期间隔输出,精确到毫秒 + * + * @param betweenMs 日期间隔 + * @return XX天XX小时XX分XX秒XX毫秒 + * @since 3.0.1 + */ + public static String formatBetween(long betweenMs) { + return new BetweenFormater(betweenMs, BetweenFormater.Level.MILLSECOND).format(); + } + + /** + * 当前日期是否在日期指定范围内
+ * 起始日期和结束日期可以互换 + * + * @param date 被检查的日期 + * @param beginDate 起始日期 + * @param endDate 结束日期 + * @return 是否在范围内 + * @since 3.0.8 + */ + public static boolean isIn(Date date, Date beginDate, Date endDate) { + if (date instanceof DateTime) { + return ((DateTime) date).isIn(beginDate, endDate); + } else { + return new DateTime(date).isIn(beginDate, endDate); + } + } + + /** + * 计时,常用于记录某段代码的执行时间,单位:纳秒 + * + * @param preTime 之前记录的时间 + * @return 时间差,纳秒 + */ + public static long spendNt(long preTime) { + return System.nanoTime() - preTime; + } + + /** + * 计时,常用于记录某段代码的执行时间,单位:毫秒 + * + * @param preTime 之前记录的时间 + * @return 时间差,毫秒 + */ + public static long spendMs(long preTime) { + return System.currentTimeMillis() - preTime; + } + + /** + * 格式化成yyMMddHHmm后转换为int型 + * + * @param date 日期 + * @return int + */ + public static int toIntSecond(Date date) { + return Integer.parseInt(DateUtil.format(date, "yyMMddHHmm")); + } + + /** + * 计算指定指定时间区间内的周数 + * + * @param start 开始时间 + * @param end 结束时间 + * @return 周数 + */ + public static int weekCount(Date start, Date end) { + final Calendar startCalendar = Calendar.getInstance(); + startCalendar.setTime(start); + final Calendar endCalendar = Calendar.getInstance(); + endCalendar.setTime(end); + + final int startWeekofYear = startCalendar.get(Calendar.WEEK_OF_YEAR); + final int endWeekofYear = endCalendar.get(Calendar.WEEK_OF_YEAR); + + int count = endWeekofYear - startWeekofYear + 1; + + if (Calendar.SUNDAY != startCalendar.get(Calendar.DAY_OF_WEEK)) { + count--; + } + + return count; + } + + /** + * 计时器
+ * 计算某个过程花费的时间,精确到毫秒 + * + * @return Timer + */ + public static TimeInterval timer() { + return new TimeInterval(); + + } + + /** + * 生日转为年龄,计算法定年龄 + * + * @param birthDay 生日,标准日期字符串 + * @return 年龄 + */ + public static int ageOfNow(String birthDay) { + return ageOfNow(parse(birthDay)); + } + + /** + * 生日转为年龄,计算法定年龄 + * + * @param birthDay 生日 + * @return 年龄 + */ + public static int ageOfNow(Date birthDay) { + return age(birthDay, date()); + } + + /** + * 计算相对于dateToCompare的年龄,长用于计算指定生日在某年的年龄 + * + * @param birthDay 生日 + * @param dateToCompare 需要对比的日期 + * @return 年龄 + */ + public static int age(Date birthDay, Date dateToCompare) { + Calendar cal = Calendar.getInstance(); + cal.setTime(dateToCompare); + + if (cal.before(birthDay)) { + throw new IllegalArgumentException(StringUtil.format("Birthday is after date {}!", formatDate(dateToCompare))); + } + + int year = cal.get(Calendar.YEAR); + int month = cal.get(Calendar.MONTH); + int dayOfMonth = cal.get(Calendar.DAY_OF_MONTH); + + cal.setTime(birthDay); + int age = year - cal.get(Calendar.YEAR); + + int monthBirth = cal.get(Calendar.MONTH); + if (month == monthBirth) { + int dayOfMonthBirth = cal.get(Calendar.DAY_OF_MONTH); + if (dayOfMonth < dayOfMonthBirth) { + // 如果生日在当月,但是未达到生日当天的日期,年龄减一 + age--; + } + } else if (month < monthBirth) { + // 如果当前月份未达到生日的月份,年龄计算减一 + age--; + } + + return age; + } + + /** + * 是否闰年 + * + * @param year 年 + * @return 是否闰年 + */ + public static boolean isLeapYear(int year) { + return new GregorianCalendar().isLeapYear(year); + } + + /** + * 判定给定开始时间经过某段时间后是否过期 + * + * @param startDate 开始时间 + * @param dateField 时间单位 + * @param timeLength 时长 + * @param checkedDate 被比较的时间。如果经过时长后的时间晚于被检查的时间,就表示过期 + * @return 是否过期 + * @since 3.1.1 + */ + public static boolean isExpired(Date startDate, DateField dateField, int timeLength, Date checkedDate) { + final Date endDate = offset(startDate, dateField, timeLength); + return endDate.after(checkedDate); + } + + /** + * 时间格式字符串转为秒数
+ * 参考:https://github.com/iceroot + * + * @param timeStr 字符串时分秒(HH:mm:ss)格式 + * @return 时分秒转换后的秒数 + * @since 3.1.2 + */ + public static int timeToSecond(String timeStr) { + if(StringUtil.isEmpty(timeStr)) { + return 0; + } + + final List hms = StringUtil.splitTrim(timeStr, StringPool.COLON, 3); + int lastIndex = hms.size() - 1; + + int result = 0; + for(int i = lastIndex; i >= 0; i--) { + result += Integer.parseInt(hms.get(i)) * Math.pow(60, (lastIndex - i)); + } + return result; + } + + /** + * 秒数转为时间格式(HH:mm:ss)
+ * 参考:https://github.com/iceroot + * + * @param seconds 需要转换的秒数 + * @return 转换后的字符串 + * @since 3.1.2 + */ + public static String secondToTime(int seconds) { + if(seconds < 0) { + throw new IllegalArgumentException("Seconds must be a positive number!"); + } + + int hour = seconds / 3600; + int other = seconds % 3600; + int minute = other / 60; + int second = other % 60; + final StringBuilder sb = new StringBuilder(); + if (hour < 10) { + sb.append("0"); + } + sb.append(hour); + sb.append(":"); + if (minute < 10) { + sb.append("0"); + } + sb.append(minute); + sb.append(":"); + if (second < 10) { + sb.append("0"); + } + sb.append(second); + return sb.toString(); + } + + // ------------------------------------------------------------------------ Private method start + /** + * 获得指定日期年份和季节
+ * 格式:[20131]表示2013年第一季度 + * + * @param cal 日期 + */ + private static String yearAndSeason(Calendar cal) { + return new StringBuilder().append(cal.get(Calendar.YEAR)).append(cal.get(Calendar.MONTH) / 3 + 1).toString(); + } + + /** + * 标准化日期,默认处理以空格区分的日期时间格式,空格前为日期,空格后为时间:
+ * 将以下字符替换为"-" + *
+	 * "."
+	 * "/"
+	 * "年"
+	 * "月"
+	 * 
+ * + * 将以下字符去除 + *
+	 * "日"
+	 * 
+ * + * 将以下字符替换为":" + *
+	 * "时"
+	 * "分"
+	 * "秒"
+	 * 
+ * 当末位是":"时去除之(不存在毫秒时) + * + * @param dateStr 日期时间字符串 + * @return 格式化后的日期字符串 + */ + private static String normalize(String dateStr) { + if(StringUtil.isBlank(dateStr)) { + return dateStr; + } + + //日期时间分开处理 + final List dateAndTime = StringUtil.splitTrim(dateStr, ' '); + final int size = dateAndTime.size(); + if(size < 1 || size > 2) { + //非可被标准处理的格式 + return dateStr; + } + + final StringBuilder builder = new StringBuilder(); + + //日期部分("\"、"/"、"."、"年"、"月"都替换为"-") + String datePart = dateAndTime.get(0).replaceAll("[\\/.年月]", "-"); + datePart = StringUtil.removeSuffix(datePart, "日"); + builder.append(datePart); + + //时间部分 + if(size == 2) { + builder.append(' '); + String timePart = dateAndTime.get(1).replaceAll("[时分秒]", ":"); + timePart = StringUtil.removeSuffix(timePart, ":"); + builder.append(timePart); + } + + return builder.toString(); + } + // ------------------------------------------------------------------------ Private method end +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/date/Month.java b/blade-core-tool/src/main/java/org/springblade/core/tool/date/Month.java new file mode 100644 index 0000000..f4358bf --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/date/Month.java @@ -0,0 +1,118 @@ +package org.springblade.core.tool.date; + +import java.util.Calendar; + +/** + * 月份枚举
+ * 与Calendar中的月份int值对应 + * + * @see Calendar#JANUARY + * @see Calendar#FEBRUARY + * @see Calendar#MARCH + * @see Calendar#APRIL + * @see Calendar#MAY + * @see Calendar#JUNE + * @see Calendar#JULY + * @see Calendar#AUGUST + * @see Calendar#SEPTEMBER + * @see Calendar#OCTOBER + * @see Calendar#NOVEMBER + * @see Calendar#DECEMBER + * @see Calendar#UNDECIMBER + * + * @author Looly + * + */ +public enum Month { + + /** 一月 */ + JANUARY(Calendar.JANUARY), + /** 二月 */ + FEBRUARY(Calendar.FEBRUARY), + /** 三月 */ + MARCH(Calendar.MARCH), + /** 四月 */ + APRIL(Calendar.APRIL), + /** 五月 */ + MAY(Calendar.MAY), + /** 六月 */ + JUNE(Calendar.JUNE), + /** 七月 */ + JULY(Calendar.JULY), + /** 八月 */ + AUGUST(Calendar.AUGUST), + /** 九月 */ + SEPTEMBER(Calendar.SEPTEMBER), + /** 十月 */ + OCTOBER(Calendar.OCTOBER), + /** 十一月 */ + NOVEMBER(Calendar.NOVEMBER), + /** 十二月 */ + DECEMBER(Calendar.DECEMBER), + /** 十三月,仅用于农历 */ + UNDECIMBER(Calendar.UNDECIMBER); + + // --------------------------------------------------------------- + private int value; + + private Month(int value) { + this.value = value; + } + + public int getValue() { + return this.value; + } + + /** + * 将 {@link Calendar}月份相关值转换为Month枚举对象
+ * + * @see Calendar#JANUARY + * @see Calendar#FEBRUARY + * @see Calendar#MARCH + * @see Calendar#APRIL + * @see Calendar#MAY + * @see Calendar#JUNE + * @see Calendar#JULY + * @see Calendar#AUGUST + * @see Calendar#SEPTEMBER + * @see Calendar#OCTOBER + * @see Calendar#NOVEMBER + * @see Calendar#DECEMBER + * @see Calendar#UNDECIMBER + * + * @param calendarMonthIntValue Calendar中关于Month的int值 + * @return {@link Month} + */ + public static Month of(int calendarMonthIntValue) { + switch (calendarMonthIntValue) { + case Calendar.JANUARY: + return JANUARY; + case Calendar.FEBRUARY: + return FEBRUARY; + case Calendar.MARCH: + return MARCH; + case Calendar.APRIL: + return APRIL; + case Calendar.MAY: + return MAY; + case Calendar.JUNE: + return JUNE; + case Calendar.JULY: + return JULY; + case Calendar.AUGUST: + return AUGUST; + case Calendar.SEPTEMBER: + return SEPTEMBER; + case Calendar.OCTOBER: + return OCTOBER; + case Calendar.NOVEMBER: + return NOVEMBER; + case Calendar.DECEMBER: + return DECEMBER; + case Calendar.UNDECIMBER: + return UNDECIMBER; + default: + return null; + } + } +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/date/Season.java b/blade-core-tool/src/main/java/org/springblade/core/tool/date/Season.java new file mode 100644 index 0000000..9cb0e75 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/date/Season.java @@ -0,0 +1,61 @@ +package org.springblade.core.tool.date; + +/** + * 季度枚举
+ * + * @see #SPRING + * @see #SUMMER + * @see #AUTUMN + * @see #WINTER + * + * @author Looly + * + */ +public enum Season { + + /** 春季(第一季度) */ + SPRING(1), + /** 夏季(第二季度) */ + SUMMER(2), + /** 秋季(第三季度) */ + AUTUMN(3), + /** 冬季(第四季度) */ + WINTER(4); + + // --------------------------------------------------------------- + private int value; + + private Season(int value) { + this.value = value; + } + + public int getValue() { + return this.value; + } + + /** + * 将 季度int转换为Season枚举对象
+ * + * @see #SPRING + * @see #SUMMER + * @see #AUTUMN + * @see #WINTER + * + * @param intValue 季度int表示 + * @return {@link Season} + */ + public static Season of(int intValue) { + switch (intValue) { + case 1: + return SPRING; + case 2: + return SUMMER; + case 3: + return AUTUMN; + case 4: + return WINTER; + default: + return null; + } + } +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/date/SystemClock.java b/blade-core-tool/src/main/java/org/springblade/core/tool/date/SystemClock.java new file mode 100644 index 0000000..2841904 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/date/SystemClock.java @@ -0,0 +1,94 @@ +package org.springblade.core.tool.date; + +import java.sql.Timestamp; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; + +/** + * 系统时钟
+ * 高并发场景下System.currentTimeMillis()的性能问题的优化 + * System.currentTimeMillis()的调用比new一个普通对象要耗时的多(具体耗时高出多少我还没测试过,有人说是100倍左右) + * System.currentTimeMillis()之所以慢是因为去跟系统打了一次交道 + * 后台定时更新时钟,JVM退出时,线程自动回收 + * + * see: http://git.oschina.net/yu120/sequence + * @author lry,looly + */ +public class SystemClock { + + /** 时钟更新间隔,单位毫秒 */ + private final long period; + /** 现在时刻的毫秒数 */ + private volatile long now; + + /** + * 构造 + * @param period + */ + private SystemClock(long period) { + this.period = period; + this.now = System.currentTimeMillis(); + scheduleClockUpdating(); + } + + /** + * 开启计时器线程 + */ + private void scheduleClockUpdating() { + ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(new ThreadFactory(){ + @Override + public Thread newThread(Runnable runnable) { + Thread thread = new Thread(runnable, "System Clock"); + thread.setDaemon(true); + return thread; + } + }); + scheduler.scheduleAtFixedRate(new Runnable(){ + @Override + public void run() { + now = System.currentTimeMillis(); + } + }, period, period, TimeUnit.MILLISECONDS); + } + + /** + * @return 当前时间毫秒数 + */ + private long currentTimeMillis() { + return now; + } + + //------------------------------------------------------------------------ static + /** + * 单例 + * @author Looly + * + */ + private static class InstanceHolder { + public static final SystemClock INSTANCE = new SystemClock(1); + } + + /** + * 单例实例 + * @return 单例实例 + */ + private static SystemClock instance() { + return InstanceHolder.INSTANCE; + } + + /** + * @return 当前时间 + */ + public static long now() { + return instance().currentTimeMillis(); + } + + /** + * @return 当前时间字符串表现形式 + */ + public static String nowDate() { + return new Timestamp(instance().currentTimeMillis()).toString(); + } +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/date/TimeInterval.java b/blade-core-tool/src/main/java/org/springblade/core/tool/date/TimeInterval.java new file mode 100644 index 0000000..0d6ec58 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/date/TimeInterval.java @@ -0,0 +1,109 @@ +package org.springblade.core.tool.date; + +/** + * 计时器
+ * 计算某个过程花费的时间,精确到毫秒 + * + * @author Looly + * + */ +public class TimeInterval { + private long time; + private boolean isNano; + + public TimeInterval() { + this(false); + } + + public TimeInterval(boolean isNano) { + this.isNano = isNano; + start(); + } + + /** + * @return 开始计时并返回当前时间 + */ + public long start() { + time = DateUtil.current(isNano); + return time; + } + + /** + * @return 重新计时并返回从开始到当前的持续时间 + */ + public long intervalRestart() { + long now = DateUtil.current(isNano); + long d = now - time; + time = now; + return d; + } + + /** + * 重新开始计算时间(重置开始时间) + * @return this + * @since 3.0.1 + */ + public TimeInterval restart(){ + time = DateUtil.current(isNano); + return this; + } + + //----------------------------------------------------------- Interval + /** + * 从开始到当前的间隔时间(毫秒数)
+ * 如果使用纳秒计时,返回纳秒差,否则返回毫秒差 + * @return 从开始到当前的间隔时间(毫秒数) + */ + public long interval() { + return DateUtil.current(isNano) - time; + } + + /** + * 从开始到当前的间隔时间(毫秒数) + * @return 从开始到当前的间隔时间(毫秒数) + */ + public long intervalMs() { + return isNano ? interval() / 1000000L : interval(); + } + + /** + * 从开始到当前的间隔秒数,取绝对值 + * @return 从开始到当前的间隔秒数,取绝对值 + */ + public long intervalSecond(){ + return intervalMs() / DateUnit.SECOND.getMillis(); + } + + /** + * 从开始到当前的间隔分钟数,取绝对值 + * @return 从开始到当前的间隔分钟数,取绝对值 + */ + public long intervalMinute(){ + return intervalMs() / DateUnit.MINUTE.getMillis(); + } + + /** + * 从开始到当前的间隔小时数,取绝对值 + * @return 从开始到当前的间隔小时数,取绝对值 + */ + public long intervalHour(){ + return intervalMs() / DateUnit.HOUR.getMillis(); + } + + /** + * 从开始到当前的间隔天数,取绝对值 + * @return 从开始到当前的间隔天数,取绝对值 + */ + public long intervalDay(){ + return intervalMs() / DateUnit.DAY.getMillis(); + } + + /** + * 从开始到当前的间隔周数,取绝对值 + * @return 从开始到当前的间隔周数,取绝对值 + */ + public long intervalWeek(){ + return intervalMs() / DateUnit.WEEK.getMillis(); + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/date/Week.java b/blade-core-tool/src/main/java/org/springblade/core/tool/date/Week.java new file mode 100644 index 0000000..caaa504 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/date/Week.java @@ -0,0 +1,120 @@ +package org.springblade.core.tool.date; + +import java.util.Calendar; + +/** + * 星期枚举
+ * 与Calendar中的星期int值对应 + * + * @see #SUNDAY + * @see #MONDAY + * @see #TUESDAY + * @see #WEDNESDAY + * @see #THURSDAY + * @see #FRIDAY + * @see #SATURDAY + * + * @author Looly + * + */ +public enum Week { + + /** 周日 */ + SUNDAY(Calendar.SUNDAY), + /** 周一 */ + MONDAY(Calendar.MONDAY), + /** 周二 */ + TUESDAY(Calendar.TUESDAY), + /** 周三 */ + WEDNESDAY(Calendar.WEDNESDAY), + /** 周四 */ + THURSDAY(Calendar.THURSDAY), + /** 周五 */ + FRIDAY(Calendar.FRIDAY), + /** 周六 */ + SATURDAY(Calendar.SATURDAY); + + // --------------------------------------------------------------- + /** 星期对应{@link Calendar} 中的Week值 */ + private int value; + + /** + * 构造 + * + * @param value 星期对应{@link Calendar} 中的Week值 + */ + private Week(int value) { + this.value = value; + } + + /** + * 获得星期对应{@link Calendar} 中的Week值 + * + * @return 星期对应{@link Calendar} 中的Week值 + */ + public int getValue() { + return this.value; + } + + /** + * 转换为中文名 + * + * @return 星期的中文名 + * @since 3.3.0 + */ + public String toChinese() { + switch (this) { + case SUNDAY: + return "星期日"; + case MONDAY: + return "星期一"; + case TUESDAY: + return "星期二"; + case WEDNESDAY: + return "星期三"; + case THURSDAY: + return "星期四"; + case FRIDAY: + return "星期五"; + case SATURDAY: + return "星期六"; + default: + return null; + } + } + + /** + * 将 {@link Calendar}星期相关值转换为Week枚举对象
+ * + * @see #SUNDAY + * @see #MONDAY + * @see #TUESDAY + * @see #WEDNESDAY + * @see #THURSDAY + * @see #FRIDAY + * @see #SATURDAY + * + * @param calendarWeekIntValue Calendar中关于Week的int值 + * @return {@link Week} + */ + public static Week of(int calendarWeekIntValue) { + switch (calendarWeekIntValue) { + case Calendar.SUNDAY: + return SUNDAY; + case Calendar.MONDAY: + return MONDAY; + case Calendar.TUESDAY: + return TUESDAY; + case Calendar.WEDNESDAY: + return WEDNESDAY; + case Calendar.THURSDAY: + return THURSDAY; + case Calendar.FRIDAY: + return FRIDAY; + case Calendar.SATURDAY: + return SATURDAY; + default: + return null; + } + } +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/date/format/AbstractDateBasic.java b/blade-core-tool/src/main/java/org/springblade/core/tool/date/format/AbstractDateBasic.java new file mode 100644 index 0000000..2c2d10e --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/date/format/AbstractDateBasic.java @@ -0,0 +1,64 @@ +package org.springblade.core.tool.date.format; + +import java.io.Serializable; +import java.util.Locale; +import java.util.TimeZone; + +public abstract class AbstractDateBasic implements DateBasic, Serializable { + private static final long serialVersionUID = 6333136319870641818L; + + /** The pattern */ + protected final String pattern; + /** The time zone. */ + protected final TimeZone timeZone; + /** The locale. */ + protected final Locale locale; + + /** + * 构造,内部使用 + * @param pattern 使用{@link java.text.SimpleDateFormat} 相同的日期格式 + * @param timeZone 非空时区{@link TimeZone} + * @param locale 非空{@link Locale} 日期地理位置 + */ + protected AbstractDateBasic(final String pattern, final TimeZone timeZone, final Locale locale) { + this.pattern = pattern; + this.timeZone = timeZone; + this.locale = locale; + } + + // ----------------------------------------------------------------------- Accessors + @Override + public String getPattern() { + return pattern; + } + + @Override + public TimeZone getTimeZone() { + return timeZone; + } + + @Override + public Locale getLocale() { + return locale; + } + + // ----------------------------------------------------------------------- Basics + @Override + public boolean equals(final Object obj) { + if (obj instanceof FastDatePrinter == false) { + return false; + } + final AbstractDateBasic other = (AbstractDateBasic) obj; + return pattern.equals(other.pattern) && timeZone.equals(other.timeZone) && locale.equals(other.locale); + } + + @Override + public int hashCode() { + return pattern.hashCode() + 13 * (timeZone.hashCode() + 13 * locale.hashCode()); + } + + @Override + public String toString() { + return "FastDatePrinter[" + pattern + "," + locale + "," + timeZone.getID() + "]"; + } +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/date/format/DateBasic.java b/blade-core-tool/src/main/java/org/springblade/core/tool/date/format/DateBasic.java new file mode 100644 index 0000000..5dc9463 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/date/format/DateBasic.java @@ -0,0 +1,33 @@ +package org.springblade.core.tool.date.format; + +import java.util.Locale; +import java.util.TimeZone; + +/** + * 日期基本信息获取接口 + * + * @author Looly + * @since 2.16.2 + */ +public interface DateBasic { + /** + * 获得日期格式化或者转换的格式 + * + * @return {@link java.text.SimpleDateFormat}兼容的格式 + */ + String getPattern(); + + /** + * 获得时区 + * + * @return {@link TimeZone} + */ + TimeZone getTimeZone(); + + /** + * 获得 日期地理位置 + * + * @return {@link Locale} + */ + Locale getLocale(); +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/date/format/DateParser.java b/blade-core-tool/src/main/java/org/springblade/core/tool/date/format/DateParser.java new file mode 100644 index 0000000..edc05be --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/date/format/DateParser.java @@ -0,0 +1,68 @@ +package org.springblade.core.tool.date.format; + +import java.text.ParseException; +import java.text.ParsePosition; +import java.util.Calendar; +import java.util.Date; + +/** + * 日期解析接口,用于解析日期字符串为 {@link Date} 对象
+ * Thanks to Apache Commons Lang 3.5 + * @since 2.16.2 + */ +public interface DateParser extends DateBasic { + + /** + * 将日期字符串解析并转换为 {@link Date} 对象
+ * 等价于 {@link java.text.DateFormat#parse(String)} + * + * @param source 日期字符串 + * @return {@link Date} + * @throws ParseException 转换异常,被转换的字符串格式错误。 + */ + Date parse(String source) throws ParseException; + + /** + * 将日期字符串解析并转换为 {@link Date} 对象
+ * 等价于 {@link java.text.DateFormat#parse(String, ParsePosition)} + * + * @param source 日期字符串 + * @param pos {@link ParsePosition} + * @return {@link Date} + */ + Date parse(String source, ParsePosition pos); + + /** + * 根据给定格式转换日期字符串 + * Updates the Calendar with parsed fields. Upon success, the ParsePosition index is updated to indicate how much of the source text was consumed. + * Not all source text needs to be consumed. + * Upon parse failure, ParsePosition error index is updated to the offset of the source text which does not match the supplied format. + * + * @param source 被转换的日期字符串 + * @param pos 定义开始转换的位置,转换结束后更新转换到的位置 + * @param calendar The calendar into which to set parsed fields. + * @return true, if source has been parsed (pos parsePosition is updated); otherwise false (and pos errorIndex is updated) + * @throws IllegalArgumentException when Calendar has been set to be not lenient, and a parsed field is out of range. + */ + boolean parse(String source, ParsePosition pos, Calendar calendar); + + /** + * 将日期字符串解析并转换为 {@link Date} 对象
+ * + * @param source A String whose beginning should be parsed. + * @return a java.util.Date object + * @throws ParseException if the beginning of the specified string cannot be parsed. + * @see java.text.DateFormat#parseObject(String) + */ + Object parseObject(String source) throws ParseException; + + /** + * 根据 {@link ParsePosition} 给定将日期字符串解析并转换为 {@link Date} 对象
+ * + * @param source A String whose beginning should be parsed. + * @param pos the parse position + * @return a java.util.Date object + * @see java.text.DateFormat#parseObject(String, ParsePosition) + */ + Object parseObject(String source, ParsePosition pos); +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/date/format/DatePrinter.java b/blade-core-tool/src/main/java/org/springblade/core/tool/date/format/DatePrinter.java new file mode 100644 index 0000000..decc2ef --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/date/format/DatePrinter.java @@ -0,0 +1,78 @@ +package org.springblade.core.tool.date.format; + +import java.util.Calendar; +import java.util.Date; + +/** + * 日期格式化输出接口
+ * Thanks to Apache Commons Lang 3.5 + * @author Looly + * @since 2.16.2 + */ +public interface DatePrinter extends DateBasic { + + /** + * 格式化日期表示的毫秒数 + * + * @param millis 日期毫秒数 + * @return the formatted string + * @since 2.1 + */ + String format(long millis); + + /** + * 使用 {@code GregorianCalendar} 格式化 {@code Date} + * + * @param date 日期 {@link Date} + * @return 格式化后的字符串 + */ + String format(Date date); + + /** + *

+ * Formats a {@code Calendar} object. + *

+ * 格式化 {@link Calendar} + * + * @param calendar {@link Calendar} + * @return 格式化后的字符串 + */ + String format(Calendar calendar); + + /** + *

+ * Formats a millisecond {@code long} value into the supplied {@code Appendable}. + *

+ * + * @param millis the millisecond value to format + * @param buf the buffer to format into + * @param the Appendable class type, usually StringBuilder or StringBuffer. + * @return the specified string buffer + */ + B format(long millis, B buf); + + /** + *

+ * Formats a {@code Date} object into the supplied {@code Appendable} using a {@code GregorianCalendar}. + *

+ * + * @param date the date to format + * @param buf the buffer to format into + * @param the Appendable class type, usually StringBuilder or StringBuffer. + * @return the specified string buffer + */ + B format(Date date, B buf); + + /** + *

+ * Formats a {@code Calendar} object into the supplied {@code Appendable}. + *

+ * The TimeZone set on the Calendar is only used to adjust the time offset. The TimeZone specified during the construction of the Parser will determine the TimeZone used in the formatted string. + * + * @param calendar the calendar to format + * @param buf the buffer to format into + * @param the Appendable class type, usually StringBuilder or StringBuffer. + * @return the specified string buffer + */ + B format(Calendar calendar, B buf); +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/date/format/FastDateFormat.java b/blade-core-tool/src/main/java/org/springblade/core/tool/date/format/FastDateFormat.java new file mode 100644 index 0000000..dfc685d --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/date/format/FastDateFormat.java @@ -0,0 +1,392 @@ +package org.springblade.core.tool.date.format; + +import java.text.*; +import java.util.Calendar; +import java.util.Date; +import java.util.Locale; +import java.util.TimeZone; + +/** + *

+ * FastDateFormat 是一个线程安全的 {@link SimpleDateFormat} 实现。 + *

+ * + *

+ * 通过以下静态方法获得此对象:
+ * {@link #getInstance(String, TimeZone, Locale)}
+ * {@link #getDateInstance(int, TimeZone, Locale)}
+ * {@link #getTimeInstance(int, TimeZone, Locale)}
+ * {@link #getDateTimeInstance(int, int, TimeZone, Locale)} + *

+ * + * Thanks to Apache Commons Lang 3.5 + * @since 2.16.2 + */ +public class FastDateFormat extends Format implements DateParser, DatePrinter { + private static final long serialVersionUID = 8097890768636183236L; + + /** FULL locale dependent date or time style. */ + public static final int FULL = DateFormat.FULL; + /** LONG locale dependent date or time style. */ + public static final int LONG = DateFormat.LONG; + /** MEDIUM locale dependent date or time style. */ + public static final int MEDIUM = DateFormat.MEDIUM; + /** SHORT locale dependent date or time style. */ + public static final int SHORT = DateFormat.SHORT; + + private static final FormatCache cache = new FormatCache(){ + @Override + protected FastDateFormat createInstance(final String pattern, final TimeZone timeZone, final Locale locale) { + return new FastDateFormat(pattern, timeZone, locale); + } + }; + + private final FastDatePrinter printer; + private final FastDateParser parser; + + // ----------------------------------------------------------------------- + /** + * 获得 {@link FastDateFormat} 实例,使用默认格式和地区 + * + * @return {@link FastDateFormat} + */ + public static FastDateFormat getInstance() { + return cache.getInstance(); + } + + /** + * 获得 {@link FastDateFormat} 实例,使用默认地区
+ * 支持缓存 + * + * @param pattern 使用{@link SimpleDateFormat} 相同的日期格式 + * @return {@link FastDateFormat} + * @throws IllegalArgumentException 日期格式问题 + */ + public static FastDateFormat getInstance(final String pattern) { + return cache.getInstance(pattern, null, null); + } + + /** + * 获得 {@link FastDateFormat} 实例
+ * 支持缓存 + * + * @param pattern 使用{@link SimpleDateFormat} 相同的日期格式 + * @param timeZone 时区{@link TimeZone} + * @return {@link FastDateFormat} + * @throws IllegalArgumentException 日期格式问题 + */ + public static FastDateFormat getInstance(final String pattern, final TimeZone timeZone) { + return cache.getInstance(pattern, timeZone, null); + } + + /** + * 获得 {@link FastDateFormat} 实例
+ * 支持缓存 + * + * @param pattern 使用{@link SimpleDateFormat} 相同的日期格式 + * @param locale {@link Locale} 日期地理位置 + * @return {@link FastDateFormat} + * @throws IllegalArgumentException 日期格式问题 + */ + public static FastDateFormat getInstance(final String pattern, final Locale locale) { + return cache.getInstance(pattern, null, locale); + } + + /** + * 获得 {@link FastDateFormat} 实例
+ * 支持缓存 + * + * @param pattern 使用{@link SimpleDateFormat} 相同的日期格式 + * @param timeZone 时区{@link TimeZone} + * @param locale {@link Locale} 日期地理位置 + * @return {@link FastDateFormat} + * @throws IllegalArgumentException 日期格式问题 + */ + public static FastDateFormat getInstance(final String pattern, final TimeZone timeZone, final Locale locale) { + return cache.getInstance(pattern, timeZone, locale); + } + + // ----------------------------------------------------------------------- + /** + * 获得 {@link FastDateFormat} 实例
+ * 支持缓存 + * + * @param style date style: FULL, LONG, MEDIUM, or SHORT + * @return 本地化 {@link FastDateFormat} + */ + public static FastDateFormat getDateInstance(final int style) { + return cache.getDateInstance(style, null, null); + } + + /** + * 获得 {@link FastDateFormat} 实例
+ * 支持缓存 + * + * @param style date style: FULL, LONG, MEDIUM, or SHORT + * @param locale {@link Locale} 日期地理位置 + * @return 本地化 {@link FastDateFormat} + */ + public static FastDateFormat getDateInstance(final int style, final Locale locale) { + return cache.getDateInstance(style, null, locale); + } + + /** + * 获得 {@link FastDateFormat} 实例
+ * 支持缓存 + * + * @param style date style: FULL, LONG, MEDIUM, or SHORT + * @param timeZone 时区{@link TimeZone} + * @return 本地化 {@link FastDateFormat} + */ + public static FastDateFormat getDateInstance(final int style, final TimeZone timeZone) { + return cache.getDateInstance(style, timeZone, null); + } + + /** + * 获得 {@link FastDateFormat} 实例
+ * 支持缓存 + * + * @param style date style: FULL, LONG, MEDIUM, or SHORT + * @param timeZone 时区{@link TimeZone} + * @param locale {@link Locale} 日期地理位置 + * @return 本地化 {@link FastDateFormat} + */ + public static FastDateFormat getDateInstance(final int style, final TimeZone timeZone, final Locale locale) { + return cache.getDateInstance(style, timeZone, locale); + } + + // ----------------------------------------------------------------------- + /** + * 获得 {@link FastDateFormat} 实例
+ * 支持缓存 + * + * @param style time style: FULL, LONG, MEDIUM, or SHORT + * @return 本地化 {@link FastDateFormat} + */ + public static FastDateFormat getTimeInstance(final int style) { + return cache.getTimeInstance(style, null, null); + } + + /** + * 获得 {@link FastDateFormat} 实例
+ * 支持缓存 + * + * @param style time style: FULL, LONG, MEDIUM, or SHORT + * @param locale {@link Locale} 日期地理位置 + * @return 本地化 {@link FastDateFormat} + */ + public static FastDateFormat getTimeInstance(final int style, final Locale locale) { + return cache.getTimeInstance(style, null, locale); + } + + /** + * 获得 {@link FastDateFormat} 实例
+ * 支持缓存 + * + * @param style time style: FULL, LONG, MEDIUM, or SHORT + * @param timeZone optional time zone, overrides time zone of formatted time + * @return 本地化 {@link FastDateFormat} + */ + public static FastDateFormat getTimeInstance(final int style, final TimeZone timeZone) { + return cache.getTimeInstance(style, timeZone, null); + } + + /** + * 获得 {@link FastDateFormat} 实例
+ * 支持缓存 + * + * @param style time style: FULL, LONG, MEDIUM, or SHORT + * @param timeZone optional time zone, overrides time zone of formatted time + * @param locale {@link Locale} 日期地理位置 + * @return 本地化 {@link FastDateFormat} + */ + public static FastDateFormat getTimeInstance(final int style, final TimeZone timeZone, final Locale locale) { + return cache.getTimeInstance(style, timeZone, locale); + } + + // ----------------------------------------------------------------------- + /** + * 获得 {@link FastDateFormat} 实例
+ * 支持缓存 + * + * @param dateStyle date style: FULL, LONG, MEDIUM, or SHORT + * @param timeStyle time style: FULL, LONG, MEDIUM, or SHORT + * @return 本地化 {@link FastDateFormat} + */ + public static FastDateFormat getDateTimeInstance(final int dateStyle, final int timeStyle) { + return cache.getDateTimeInstance(dateStyle, timeStyle, null, null); + } + + /** + * 获得 {@link FastDateFormat} 实例
+ * 支持缓存 + * + * @param dateStyle date style: FULL, LONG, MEDIUM, or SHORT + * @param timeStyle time style: FULL, LONG, MEDIUM, or SHORT + * @param locale {@link Locale} 日期地理位置 + * @return 本地化 {@link FastDateFormat} + */ + public static FastDateFormat getDateTimeInstance(final int dateStyle, final int timeStyle, final Locale locale) { + return cache.getDateTimeInstance(dateStyle, timeStyle, null, locale); + } + + /** + * 获得 {@link FastDateFormat} 实例
+ * 支持缓存 + * + * @param dateStyle date style: FULL, LONG, MEDIUM, or SHORT + * @param timeStyle time style: FULL, LONG, MEDIUM, or SHORT + * @param timeZone 时区{@link TimeZone} + * @return 本地化 {@link FastDateFormat} + */ + public static FastDateFormat getDateTimeInstance(final int dateStyle, final int timeStyle, final TimeZone timeZone) { + return getDateTimeInstance(dateStyle, timeStyle, timeZone, null); + } + + /** + * 获得 {@link FastDateFormat} 实例
+ * 支持缓存 + * + * @param dateStyle date style: FULL, LONG, MEDIUM, or SHORT + * @param timeStyle time style: FULL, LONG, MEDIUM, or SHORT + * @param timeZone 时区{@link TimeZone} + * @param locale {@link Locale} 日期地理位置 + * @return 本地化 {@link FastDateFormat} + */ + public static FastDateFormat getDateTimeInstance(final int dateStyle, final int timeStyle, final TimeZone timeZone, final Locale locale) { + return cache.getDateTimeInstance(dateStyle, timeStyle, timeZone, locale); + } + + // ----------------------------------------------------------------------- Constructor start + /** + * 构造 + * + * @param pattern 使用{@link SimpleDateFormat} 相同的日期格式 + * @param timeZone 非空时区 {@link TimeZone} + * @param locale {@link Locale} 日期地理位置 + * @throws NullPointerException if pattern, timeZone, or locale is null. + */ + protected FastDateFormat(final String pattern, final TimeZone timeZone, final Locale locale) { + this(pattern, timeZone, locale, null); + } + + /** + * 构造 + * + * @param pattern 使用{@link SimpleDateFormat} 相同的日期格式 + * @param timeZone 非空时区 {@link TimeZone} + * @param locale {@link Locale} 日期地理位置 + * @param centuryStart The start of the 100 year period to use as the "default century" for 2 digit year parsing. If centuryStart is null, defaults to now - 80 years + * @throws NullPointerException if pattern, timeZone, or locale is null. + */ + protected FastDateFormat(final String pattern, final TimeZone timeZone, final Locale locale, final Date centuryStart) { + printer = new FastDatePrinter(pattern, timeZone, locale); + parser = new FastDateParser(pattern, timeZone, locale, centuryStart); + } + // ----------------------------------------------------------------------- Constructor end + + // ----------------------------------------------------------------------- Format methods + @Override + public StringBuffer format(final Object obj, final StringBuffer toAppendTo, final FieldPosition pos) { + return toAppendTo.append(printer.format(obj)); + } + + @Override + public String format(final long millis) { + return printer.format(millis); + } + + @Override + public String format(final Date date) { + return printer.format(date); + } + + @Override + public String format(final Calendar calendar) { + return printer.format(calendar); + } + + @Override + public B format(final long millis, final B buf) { + return printer.format(millis, buf); + } + + @Override + public B format(final Date date, final B buf) { + return printer.format(date, buf); + } + + @Override + public B format(final Calendar calendar, final B buf) { + return printer.format(calendar, buf); + } + + // ----------------------------------------------------------------------- Parsing + @Override + public Date parse(final String source) throws ParseException { + return parser.parse(source); + } + + @Override + public Date parse(final String source, final ParsePosition pos) { + return parser.parse(source, pos); + } + + @Override + public boolean parse(final String source, final ParsePosition pos, final Calendar calendar) { + return parser.parse(source, pos, calendar); + } + + @Override + public Object parseObject(final String source, final ParsePosition pos) { + return parser.parseObject(source, pos); + } + + // ----------------------------------------------------------------------- Accessors + @Override + public String getPattern() { + return printer.getPattern(); + } + + @Override + public TimeZone getTimeZone() { + return printer.getTimeZone(); + } + + @Override + public Locale getLocale() { + return printer.getLocale(); + } + + /** + *估算生成的日期字符串长度
+ * 实际生成的字符串长度小于或等于此值 + * + * @return 日期字符串长度 + */ + public int getMaxLengthEstimate() { + return printer.getMaxLengthEstimate(); + } + + // Basics + // ----------------------------------------------------------------------- + @Override + public boolean equals(final Object obj) { + if (obj instanceof FastDateFormat == false) { + return false; + } + final FastDateFormat other = (FastDateFormat) obj; + // no need to check parser, as it has same invariants as printer + return printer.equals(other.printer); + } + + @Override + public int hashCode() { + return printer.hashCode(); + } + + @Override + public String toString() { + return "FastDateFormat[" + printer.getPattern() + "," + printer.getLocale() + "," + printer.getTimeZone().getID() + "]"; + } +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/date/format/FastDateParser.java b/blade-core-tool/src/main/java/org/springblade/core/tool/date/format/FastDateParser.java new file mode 100644 index 0000000..95a0717 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/date/format/FastDateParser.java @@ -0,0 +1,822 @@ +package org.springblade.core.tool.date.format; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.text.DateFormatSymbols; +import java.text.ParseException; +import java.text.ParsePosition; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * {@link java.text.SimpleDateFormat} 的线程安全版本,用于解析日期字符串并转换为 {@link Date} 对象
+ * Thanks to Apache Commons Lang 3.5 + * + * @since 2.16.2 + * @see FastDatePrinter + */ +class FastDateParser extends AbstractDateBasic implements DateParser { + private static final long serialVersionUID = -3199383897950947498L; + + static final Locale JAPANESE_IMPERIAL = new Locale("ja", "JP", "JP"); + + /** 世纪:2000年前为19, 之后为20 */ + private final int century; + private final int startYear; + + // derived fields + private transient List patterns; + + // comparator used to sort regex alternatives + // alternatives should be ordered longer first, and shorter last. ('february' before 'feb') + // all entries must be lowercase by locale. + private static final Comparator LONGER_FIRST_LOWERCASE = new Comparator(){ + @Override + public int compare(final String left, final String right) { + return right.compareTo(left); + } + }; + + /** + *

+ * Constructs a new FastDateParser. + *

+ * + * Use {@link FastDateFormat#getInstance(String, TimeZone, Locale)} or another variation of the factory methods of {@link FastDateFormat} to get a cached FastDateParser instance. + * + * @param pattern non-null {@link java.text.SimpleDateFormat} compatible pattern + * @param timeZone non-null time zone to use + * @param locale non-null locale + */ + protected FastDateParser(final String pattern, final TimeZone timeZone, final Locale locale) { + this(pattern, timeZone, locale, null); + } + + /** + *

+ * Constructs a new FastDateParser. + *

+ * + * @param pattern non-null {@link java.text.SimpleDateFormat} compatible pattern + * @param timeZone non-null time zone to use + * @param locale non-null locale + * @param centuryStart The start of the century for 2 digit year parsing + */ + protected FastDateParser(final String pattern, final TimeZone timeZone, final Locale locale, final Date centuryStart) { + super(pattern, timeZone, locale); + final Calendar definingCalendar = Calendar.getInstance(timeZone, locale); + + int centuryStartYear; + if (centuryStart != null) { + definingCalendar.setTime(centuryStart); + centuryStartYear = definingCalendar.get(Calendar.YEAR); + } else if (locale.equals(JAPANESE_IMPERIAL)) { + centuryStartYear = 0; + } else { + // from 80 years ago to 20 years from now + definingCalendar.setTime(new Date()); + centuryStartYear = definingCalendar.get(Calendar.YEAR) - 80; + } + century = centuryStartYear / 100 * 100; + startYear = centuryStartYear - century; + + init(definingCalendar); + } + + /** + * Initialize derived fields from defining fields. This is called from constructor and from readObject (de-serialization) + * + * @param definingCalendar the {@link Calendar} instance used to initialize this FastDateParser + */ + private void init(final Calendar definingCalendar) { + patterns = new ArrayList<>(); + + final StrategyParser fm = new StrategyParser(definingCalendar); + for (;;) { + final StrategyAndWidth field = fm.getNextStrategy(); + if (field == null) { + break; + } + patterns.add(field); + } + } + + // helper classes to parse the format string + // ----------------------------------------------------------------------- + + /** + * Holds strategy and field width + */ + private static class StrategyAndWidth { + final Strategy strategy; + final int width; + + StrategyAndWidth(final Strategy strategy, final int width) { + this.strategy = strategy; + this.width = width; + } + + int getMaxWidth(final ListIterator lt) { + if (!strategy.isNumber() || !lt.hasNext()) { + return 0; + } + final Strategy nextStrategy = lt.next().strategy; + lt.previous(); + return nextStrategy.isNumber() ? width : 0; + } + } + + /** + * Parse format into Strategies + */ + private class StrategyParser { + final private Calendar definingCalendar; + private int currentIdx; + + StrategyParser(final Calendar definingCalendar) { + this.definingCalendar = definingCalendar; + } + + StrategyAndWidth getNextStrategy() { + if (currentIdx >= pattern.length()) { + return null; + } + + final char c = pattern.charAt(currentIdx); + if (isFormatLetter(c)) { + return letterPattern(c); + } + return literal(); + } + + private StrategyAndWidth letterPattern(final char c) { + final int begin = currentIdx; + while (++currentIdx < pattern.length()) { + if (pattern.charAt(currentIdx) != c) { + break; + } + } + + final int width = currentIdx - begin; + return new StrategyAndWidth(getStrategy(c, width, definingCalendar), width); + } + + private StrategyAndWidth literal() { + boolean activeQuote = false; + + final StringBuilder sb = new StringBuilder(); + while (currentIdx < pattern.length()) { + final char c = pattern.charAt(currentIdx); + if (!activeQuote && isFormatLetter(c)) { + break; + } else if (c == '\'' && (++currentIdx == pattern.length() || pattern.charAt(currentIdx) != '\'')) { + activeQuote = !activeQuote; + continue; + } + ++currentIdx; + sb.append(c); + } + + if (activeQuote) { + throw new IllegalArgumentException("Unterminated quote"); + } + + final String formatField = sb.toString(); + return new StrategyAndWidth(new CopyQuotedStrategy(formatField), formatField.length()); + } + } + + private static boolean isFormatLetter(final char c) { + return c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z'; + } + + // Serializing + // ----------------------------------------------------------------------- + /** + * Create the object after serialization. This implementation reinitializes the transient properties. + * + * @param in ObjectInputStream from which the object is being deserialized. + * @throws IOException if there is an IO issue. + * @throws ClassNotFoundException if a class cannot be found. + */ + private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException { + in.defaultReadObject(); + + final Calendar definingCalendar = Calendar.getInstance(timeZone, locale); + init(definingCalendar); + } + + @Override + public Object parseObject(final String source) throws ParseException { + return parse(source); + } + + @Override + public Date parse(final String source) throws ParseException { + final ParsePosition pp = new ParsePosition(0); + final Date date = parse(source, pp); + if (date == null) { + // Add a note re supported date range + if (locale.equals(JAPANESE_IMPERIAL)) { + throw new ParseException("(The " + locale + " locale does not support dates before 1868 AD)\n" + "Unparseable date: \"" + source, pp.getErrorIndex()); + } + throw new ParseException("Unparseable date: " + source, pp.getErrorIndex()); + } + return date; + } + + @Override + public Object parseObject(final String source, final ParsePosition pos) { + return parse(source, pos); + } + + @Override + public Date parse(final String source, final ParsePosition pos) { + // timing tests indicate getting new instance is 19% faster than cloning + final Calendar cal = Calendar.getInstance(timeZone, locale); + cal.clear(); + + return parse(source, pos, cal) ? cal.getTime() : null; + } + + @Override + public boolean parse(final String source, final ParsePosition pos, final Calendar calendar) { + final ListIterator lt = patterns.listIterator(); + while (lt.hasNext()) { + final StrategyAndWidth strategyAndWidth = lt.next(); + final int maxWidth = strategyAndWidth.getMaxWidth(lt); + if (!strategyAndWidth.strategy.parse(this, calendar, source, pos, maxWidth)) { + return false; + } + } + return true; + } + + // Support for strategies + // ----------------------------------------------------------------------- + + private static StringBuilder simpleQuote(final StringBuilder sb, final String value) { + for (int i = 0; i < value.length(); ++i) { + final char c = value.charAt(i); + switch (c) { + case '\\': + case '^': + case '$': + case '.': + case '|': + case '?': + case '*': + case '+': + case '(': + case ')': + case '[': + case '{': + sb.append('\\'); + default: + sb.append(c); + } + } + return sb; + } + + /** + * Get the short and long values displayed for a field + * + * @param cal The calendar to obtain the short and long values + * @param locale The locale of display names + * @param field The field of interest + * @param regex The regular expression to build + * @return The map of string display names to field values + */ + private static Map appendDisplayNames(final Calendar cal, final Locale locale, final int field, final StringBuilder regex) { + final Map values = new HashMap<>(); + + final Map displayNames = cal.getDisplayNames(field, Calendar.ALL_STYLES, locale); + final TreeSet sorted = new TreeSet<>(LONGER_FIRST_LOWERCASE); + for (final Map.Entry displayName : displayNames.entrySet()) { + final String key = displayName.getKey().toLowerCase(locale); + if (sorted.add(key)) { + values.put(key, displayName.getValue()); + } + } + for (final String symbol : sorted) { + simpleQuote(regex, symbol).append('|'); + } + return values; + } + + /** + * 使用当前的世纪调整两位数年份为四位数年份 + * + * @param twoDigitYear 两位数年份 + * @return A value between centuryStart(inclusive) to centuryStart+100(exclusive) + */ + private int adjustYear(final int twoDigitYear) { + final int trial = century + twoDigitYear; + return twoDigitYear >= startYear ? trial : trial + 100; + } + + /** + * 单个日期字段的分析策略 + */ + private static abstract class Strategy { + /** + * Is this field a number? The default implementation returns false. + * + * @return true, if field is a number + */ + boolean isNumber() { + return false; + } + + abstract boolean parse(FastDateParser parser, Calendar calendar, String source, ParsePosition pos, int maxWidth); + } + + /** + * A strategy to parse a single field from the parsing pattern + */ + private static abstract class PatternStrategy extends Strategy { + + private Pattern pattern; + + void createPattern(final StringBuilder regex) { + createPattern(regex.toString()); + } + + void createPattern(final String regex) { + this.pattern = Pattern.compile(regex); + } + + /** + * Is this field a number? The default implementation returns false. + * + * @return true, if field is a number + */ + @Override + boolean isNumber() { + return false; + } + + @Override + boolean parse(final FastDateParser parser, final Calendar calendar, final String source, final ParsePosition pos, final int maxWidth) { + final Matcher matcher = pattern.matcher(source.substring(pos.getIndex())); + if (!matcher.lookingAt()) { + pos.setErrorIndex(pos.getIndex()); + return false; + } + pos.setIndex(pos.getIndex() + matcher.end(1)); + setCalendar(parser, calendar, matcher.group(1)); + return true; + } + + abstract void setCalendar(FastDateParser parser, Calendar cal, String value); + } + + /** + * Obtain a Strategy given a field from a SimpleDateFormat pattern + * + * @param formatField A sub-sequence of the SimpleDateFormat pattern + * @param definingCalendar The calendar to obtain the short and long values + * @return The Strategy that will handle parsing for the field + */ + private Strategy getStrategy(final char f, final int width, final Calendar definingCalendar) { + switch (f) { + default: + throw new IllegalArgumentException("Format '" + f + "' not supported"); + case 'D': + return DAY_OF_YEAR_STRATEGY; + case 'E': + return getLocaleSpecificStrategy(Calendar.DAY_OF_WEEK, definingCalendar); + case 'F': + return DAY_OF_WEEK_IN_MONTH_STRATEGY; + case 'G': + return getLocaleSpecificStrategy(Calendar.ERA, definingCalendar); + case 'H': // Hour in day (0-23) + return HOUR_OF_DAY_STRATEGY; + case 'K': // Hour in am/pm (0-11) + return HOUR_STRATEGY; + case 'M': + return width >= 3 ? getLocaleSpecificStrategy(Calendar.MONTH, definingCalendar) : NUMBER_MONTH_STRATEGY; + case 'S': + return MILLISECOND_STRATEGY; + case 'W': + return WEEK_OF_MONTH_STRATEGY; + case 'a': + return getLocaleSpecificStrategy(Calendar.AM_PM, definingCalendar); + case 'd': + return DAY_OF_MONTH_STRATEGY; + case 'h': // Hour in am/pm (1-12), i.e. midday/midnight is 12, not 0 + return HOUR12_STRATEGY; + case 'k': // Hour in day (1-24), i.e. midnight is 24, not 0 + return HOUR24_OF_DAY_STRATEGY; + case 'm': + return MINUTE_STRATEGY; + case 's': + return SECOND_STRATEGY; + case 'u': + return DAY_OF_WEEK_STRATEGY; + case 'w': + return WEEK_OF_YEAR_STRATEGY; + case 'y': + case 'Y': + return width > 2 ? LITERAL_YEAR_STRATEGY : ABBREVIATED_YEAR_STRATEGY; + case 'X': + return ISO8601TimeZoneStrategy.getStrategy(width); + case 'Z': + if (width == 2) { + return ISO8601TimeZoneStrategy.ISO_8601_3_STRATEGY; + } + //$FALL-THROUGH$ + case 'z': + return getLocaleSpecificStrategy(Calendar.ZONE_OFFSET, definingCalendar); + } + } + + @SuppressWarnings("unchecked") // OK because we are creating an array with no entries + private static final ConcurrentMap[] caches = new ConcurrentMap[Calendar.FIELD_COUNT]; + + /** + * Get a cache of Strategies for a particular field + * + * @param field The Calendar field + * @return a cache of Locale to Strategy + */ + private static ConcurrentMap getCache(final int field) { + synchronized (caches) { + if (caches[field] == null) { + caches[field] = new ConcurrentHashMap<>(3); + } + return caches[field]; + } + } + + /** + * Construct a Strategy that parses a Text field + * + * @param field The Calendar field + * @param definingCalendar The calendar to obtain the short and long values + * @return a TextStrategy for the field and Locale + */ + private Strategy getLocaleSpecificStrategy(final int field, final Calendar definingCalendar) { + final ConcurrentMap cache = getCache(field); + Strategy strategy = cache.get(locale); + if (strategy == null) { + strategy = field == Calendar.ZONE_OFFSET ? new TimeZoneStrategy(locale) : new CaseInsensitiveTextStrategy(field, definingCalendar, locale); + final Strategy inCache = cache.putIfAbsent(locale, strategy); + if (inCache != null) { + return inCache; + } + } + return strategy; + } + + /** + * A strategy that copies the static or quoted field in the parsing pattern + */ + private static class CopyQuotedStrategy extends Strategy { + + final private String formatField; + + /** + * Construct a Strategy that ensures the formatField has literal text + * + * @param formatField The literal text to match + */ + CopyQuotedStrategy(final String formatField) { + this.formatField = formatField; + } + + /** + * {@inheritDoc} + */ + @Override + boolean isNumber() { + return false; + } + + @Override + boolean parse(final FastDateParser parser, final Calendar calendar, final String source, final ParsePosition pos, final int maxWidth) { + for (int idx = 0; idx < formatField.length(); ++idx) { + final int sIdx = idx + pos.getIndex(); + if (sIdx == source.length()) { + pos.setErrorIndex(sIdx); + return false; + } + if (formatField.charAt(idx) != source.charAt(sIdx)) { + pos.setErrorIndex(sIdx); + return false; + } + } + pos.setIndex(formatField.length() + pos.getIndex()); + return true; + } + } + + /** + * A strategy that handles a text field in the parsing pattern + */ + private static class CaseInsensitiveTextStrategy extends PatternStrategy { + private final int field; + final Locale locale; + private final Map lKeyValues; + + /** + * Construct a Strategy that parses a Text field + * + * @param field The Calendar field + * @param definingCalendar The Calendar to use + * @param locale The Locale to use + */ + CaseInsensitiveTextStrategy(final int field, final Calendar definingCalendar, final Locale locale) { + this.field = field; + this.locale = locale; + + final StringBuilder regex = new StringBuilder(); + regex.append("((?iu)"); + lKeyValues = appendDisplayNames(definingCalendar, locale, field, regex); + regex.setLength(regex.length() - 1); + regex.append(")"); + createPattern(regex); + } + + /** + * {@inheritDoc} + */ + @Override + void setCalendar(final FastDateParser parser, final Calendar cal, final String value) { + final Integer iVal = lKeyValues.get(value.toLowerCase(locale)); + cal.set(field, iVal.intValue()); + } + } + + /** + * A strategy that handles a number field in the parsing pattern + */ + private static class NumberStrategy extends Strategy { + private final int field; + + /** + * Construct a Strategy that parses a Number field + * + * @param field The Calendar field + */ + NumberStrategy(final int field) { + this.field = field; + } + + /** + * {@inheritDoc} + */ + @Override + boolean isNumber() { + return true; + } + + @Override + boolean parse(final FastDateParser parser, final Calendar calendar, final String source, final ParsePosition pos, final int maxWidth) { + int idx = pos.getIndex(); + int last = source.length(); + + if (maxWidth == 0) { + // if no maxWidth, strip leading white space + for (; idx < last; ++idx) { + final char c = source.charAt(idx); + if (!Character.isWhitespace(c)) { + break; + } + } + pos.setIndex(idx); + } else { + final int end = idx + maxWidth; + if (last > end) { + last = end; + } + } + + for (; idx < last; ++idx) { + final char c = source.charAt(idx); + if (!Character.isDigit(c)) { + break; + } + } + + if (pos.getIndex() == idx) { + pos.setErrorIndex(idx); + return false; + } + + final int value = Integer.parseInt(source.substring(pos.getIndex(), idx)); + pos.setIndex(idx); + + calendar.set(field, modify(parser, value)); + return true; + } + + /** + * Make any modifications to parsed integer + * + * @param parser The parser + * @param iValue The parsed integer + * @return The modified value + */ + int modify(final FastDateParser parser, final int iValue) { + return iValue; + } + + } + + private static final Strategy ABBREVIATED_YEAR_STRATEGY = new NumberStrategy(Calendar.YEAR){ + /** + * {@inheritDoc} + */ + @Override + int modify(final FastDateParser parser, final int iValue) { + return iValue < 100 ? parser.adjustYear(iValue) : iValue; + } + }; + + /** + * A strategy that handles a timezone field in the parsing pattern + */ + static class TimeZoneStrategy extends PatternStrategy { + private static final String RFC_822_TIME_ZONE = "[+-]\\d{4}"; + private static final String GMT_OPTION = "GMT[+-]\\d{1,2}:\\d{2}"; + + private final Locale locale; + private final Map tzNames = new HashMap<>(); + + private static class TzInfo { + TimeZone zone; + int dstOffset; + + TzInfo(final TimeZone tz, final boolean useDst) { + zone = tz; + dstOffset = useDst ? tz.getDSTSavings() : 0; + } + } + + /** + * Index of zone id + */ + private static final int ID = 0; + + /** + * Construct a Strategy that parses a TimeZone + * + * @param locale The Locale + */ + TimeZoneStrategy(final Locale locale) { + this.locale = locale; + + final StringBuilder sb = new StringBuilder(); + sb.append("((?iu)" + RFC_822_TIME_ZONE + "|" + GMT_OPTION); + + final Set sorted = new TreeSet<>(LONGER_FIRST_LOWERCASE); + + final String[][] zones = DateFormatSymbols.getInstance(locale).getZoneStrings(); + for (final String[] zoneNames : zones) { + // offset 0 is the time zone ID and is not localized + final String tzId = zoneNames[ID]; + if (tzId.equalsIgnoreCase("GMT")) { + continue; + } + final TimeZone tz = TimeZone.getTimeZone(tzId); + // offset 1 is long standard name + // offset 2 is short standard name + final TzInfo standard = new TzInfo(tz, false); + TzInfo tzInfo = standard; + for (int i = 1; i < zoneNames.length; ++i) { + switch (i) { + case 3: // offset 3 is long daylight savings (or summertime) name + // offset 4 is the short summertime name + tzInfo = new TzInfo(tz, true); + break; + case 5: // offset 5 starts additional names, probably standard time + tzInfo = standard; + break; + } + if (zoneNames[i] != null) { + final String key = zoneNames[i].toLowerCase(locale); + // ignore the data associated with duplicates supplied in + // the additional names + if (sorted.add(key)) { + tzNames.put(key, tzInfo); + } + } + } + } + // order the regex alternatives with longer strings first, greedy + // match will ensure longest string will be consumed + for (final String zoneName : sorted) { + simpleQuote(sb.append('|'), zoneName); + } + sb.append(")"); + createPattern(sb); + } + + /** + * {@inheritDoc} + */ + @Override + void setCalendar(final FastDateParser parser, final Calendar cal, final String value) { + if (value.charAt(0) == '+' || value.charAt(0) == '-') { + final TimeZone tz = TimeZone.getTimeZone("GMT" + value); + cal.setTimeZone(tz); + } else if (value.regionMatches(true, 0, "GMT", 0, 3)) { + final TimeZone tz = TimeZone.getTimeZone(value.toUpperCase()); + cal.setTimeZone(tz); + } else { + final TzInfo tzInfo = tzNames.get(value.toLowerCase(locale)); + cal.set(Calendar.DST_OFFSET, tzInfo.dstOffset); + cal.set(Calendar.ZONE_OFFSET, tzInfo.zone.getRawOffset()); + } + } + } + + private static class ISO8601TimeZoneStrategy extends PatternStrategy { + // Z, +hh, -hh, +hhmm, -hhmm, +hh:mm or -hh:mm + + /** + * Construct a Strategy that parses a TimeZone + * + * @param pattern The Pattern + */ + ISO8601TimeZoneStrategy(final String pattern) { + createPattern(pattern); + } + + /** + * {@inheritDoc} + */ + @Override + void setCalendar(final FastDateParser parser, final Calendar cal, final String value) { + if (value.equals("Z")) { + cal.setTimeZone(TimeZone.getTimeZone("UTC")); + } else { + cal.setTimeZone(TimeZone.getTimeZone("GMT" + value)); + } + } + + private static final Strategy ISO_8601_1_STRATEGY = new ISO8601TimeZoneStrategy("(Z|(?:[+-]\\d{2}))"); + private static final Strategy ISO_8601_2_STRATEGY = new ISO8601TimeZoneStrategy("(Z|(?:[+-]\\d{2}\\d{2}))"); + private static final Strategy ISO_8601_3_STRATEGY = new ISO8601TimeZoneStrategy("(Z|(?:[+-]\\d{2}(?::)\\d{2}))"); + + /** + * Factory method for ISO8601TimeZoneStrategies. + * + * @param tokenLen a token indicating the length of the TimeZone String to be formatted. + * @return a ISO8601TimeZoneStrategy that can format TimeZone String of length {@code tokenLen}. If no such strategy exists, an IllegalArgumentException will be thrown. + */ + static Strategy getStrategy(final int tokenLen) { + switch (tokenLen) { + case 1: + return ISO_8601_1_STRATEGY; + case 2: + return ISO_8601_2_STRATEGY; + case 3: + return ISO_8601_3_STRATEGY; + default: + throw new IllegalArgumentException("invalid number of X"); + } + } + } + + private static final Strategy NUMBER_MONTH_STRATEGY = new NumberStrategy(Calendar.MONTH){ + @Override + int modify(final FastDateParser parser, final int iValue) { + return iValue - 1; + } + }; + private static final Strategy LITERAL_YEAR_STRATEGY = new NumberStrategy(Calendar.YEAR); + private static final Strategy WEEK_OF_YEAR_STRATEGY = new NumberStrategy(Calendar.WEEK_OF_YEAR); + private static final Strategy WEEK_OF_MONTH_STRATEGY = new NumberStrategy(Calendar.WEEK_OF_MONTH); + private static final Strategy DAY_OF_YEAR_STRATEGY = new NumberStrategy(Calendar.DAY_OF_YEAR); + private static final Strategy DAY_OF_MONTH_STRATEGY = new NumberStrategy(Calendar.DAY_OF_MONTH); + private static final Strategy DAY_OF_WEEK_STRATEGY = new NumberStrategy(Calendar.DAY_OF_WEEK){ + @Override + int modify(final FastDateParser parser, final int iValue) { + return iValue != 7 ? iValue + 1 : Calendar.SUNDAY; + } + }; + private static final Strategy DAY_OF_WEEK_IN_MONTH_STRATEGY = new NumberStrategy(Calendar.DAY_OF_WEEK_IN_MONTH); + private static final Strategy HOUR_OF_DAY_STRATEGY = new NumberStrategy(Calendar.HOUR_OF_DAY); + private static final Strategy HOUR24_OF_DAY_STRATEGY = new NumberStrategy(Calendar.HOUR_OF_DAY){ + @Override + int modify(final FastDateParser parser, final int iValue) { + return iValue == 24 ? 0 : iValue; + } + }; + private static final Strategy HOUR12_STRATEGY = new NumberStrategy(Calendar.HOUR){ + @Override + int modify(final FastDateParser parser, final int iValue) { + return iValue == 12 ? 0 : iValue; + } + }; + private static final Strategy HOUR_STRATEGY = new NumberStrategy(Calendar.HOUR); + private static final Strategy MINUTE_STRATEGY = new NumberStrategy(Calendar.MINUTE); + private static final Strategy SECOND_STRATEGY = new NumberStrategy(Calendar.SECOND); + private static final Strategy MILLISECOND_STRATEGY = new NumberStrategy(Calendar.MILLISECOND); +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/date/format/FastDatePrinter.java b/blade-core-tool/src/main/java/org/springblade/core/tool/date/format/FastDatePrinter.java new file mode 100644 index 0000000..82f5298 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/date/format/FastDatePrinter.java @@ -0,0 +1,1320 @@ +package org.springblade.core.tool.date.format; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.text.DateFormatSymbols; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * {@link java.text.SimpleDateFormat} 的线程安全版本,用于将 {@link Date} 格式化输出
+ * Thanks to Apache Commons Lang 3.5 + * + * @since 2.16.2 + * @see FastDateParser + */ +class FastDatePrinter extends AbstractDateBasic implements DatePrinter { + private static final long serialVersionUID = -6305750172255764887L; + + /** 规则列表. */ + private transient Rule[] rules; + /** 估算最大长度. */ + private transient int mMaxLengthEstimate; + + // Constructor + // ----------------------------------------------------------------------- + /** + * 构造,内部使用
+ * + * @param pattern 使用{@link java.text.SimpleDateFormat} 相同的日期格式 + * @param timeZone 非空时区{@link TimeZone} + * @param locale 非空{@link Locale} 日期地理位置 + */ + protected FastDatePrinter(final String pattern, final TimeZone timeZone, final Locale locale) { + super(pattern, timeZone, locale); + init(); + } + + /** + * 初始化 + */ + private void init() { + final List rulesList = parsePattern(); + rules = rulesList.toArray(new Rule[rulesList.size()]); + + int len = 0; + for (int i = rules.length; --i >= 0;) { + len += rules[i].estimateLength(); + } + + mMaxLengthEstimate = len; + } + + // Parse the pattern + // ----------------------------------------------------------------------- + /** + *

+ * Returns a list of Rules given a pattern. + *

+ * + * @return a {@code List} of Rule objects + * @throws IllegalArgumentException if pattern is invalid + */ + protected List parsePattern() { + final DateFormatSymbols symbols = new DateFormatSymbols(locale); + final List rules = new ArrayList<>(); + + final String[] ERAs = symbols.getEras(); + final String[] months = symbols.getMonths(); + final String[] shortMonths = symbols.getShortMonths(); + final String[] weekdays = symbols.getWeekdays(); + final String[] shortWeekdays = symbols.getShortWeekdays(); + final String[] AmPmStrings = symbols.getAmPmStrings(); + + final int length = pattern.length(); + final int[] indexRef = new int[1]; + + for (int i = 0; i < length; i++) { + indexRef[0] = i; + final String token = parseToken(pattern, indexRef); + i = indexRef[0]; + + final int tokenLen = token.length(); + if (tokenLen == 0) { + break; + } + + Rule rule; + final char c = token.charAt(0); + + switch (c) { + case 'G': // era designator (text) + rule = new TextField(Calendar.ERA, ERAs); + break; + case 'y': // year (number) + case 'Y': // week year + if (tokenLen == 2) { + rule = TwoDigitYearField.INSTANCE; + } else { + rule = selectNumberRule(Calendar.YEAR, tokenLen < 4 ? 4 : tokenLen); + } + if (c == 'Y') { + rule = new WeekYear((NumberRule) rule); + } + break; + case 'M': // month in year (text and number) + if (tokenLen >= 4) { + rule = new TextField(Calendar.MONTH, months); + } else if (tokenLen == 3) { + rule = new TextField(Calendar.MONTH, shortMonths); + } else if (tokenLen == 2) { + rule = TwoDigitMonthField.INSTANCE; + } else { + rule = UnpaddedMonthField.INSTANCE; + } + break; + case 'd': // day in month (number) + rule = selectNumberRule(Calendar.DAY_OF_MONTH, tokenLen); + break; + case 'h': // hour in am/pm (number, 1..12) + rule = new TwelveHourField(selectNumberRule(Calendar.HOUR, tokenLen)); + break; + case 'H': // hour in day (number, 0..23) + rule = selectNumberRule(Calendar.HOUR_OF_DAY, tokenLen); + break; + case 'm': // minute in hour (number) + rule = selectNumberRule(Calendar.MINUTE, tokenLen); + break; + case 's': // second in minute (number) + rule = selectNumberRule(Calendar.SECOND, tokenLen); + break; + case 'S': // millisecond (number) + rule = selectNumberRule(Calendar.MILLISECOND, tokenLen); + break; + case 'E': // day in week (text) + rule = new TextField(Calendar.DAY_OF_WEEK, tokenLen < 4 ? shortWeekdays : weekdays); + break; + case 'u': // day in week (number) + rule = new DayInWeekField(selectNumberRule(Calendar.DAY_OF_WEEK, tokenLen)); + break; + case 'D': // day in year (number) + rule = selectNumberRule(Calendar.DAY_OF_YEAR, tokenLen); + break; + case 'F': // day of week in month (number) + rule = selectNumberRule(Calendar.DAY_OF_WEEK_IN_MONTH, tokenLen); + break; + case 'w': // week in year (number) + rule = selectNumberRule(Calendar.WEEK_OF_YEAR, tokenLen); + break; + case 'W': // week in month (number) + rule = selectNumberRule(Calendar.WEEK_OF_MONTH, tokenLen); + break; + case 'a': // am/pm marker (text) + rule = new TextField(Calendar.AM_PM, AmPmStrings); + break; + case 'k': // hour in day (1..24) + rule = new TwentyFourHourField(selectNumberRule(Calendar.HOUR_OF_DAY, tokenLen)); + break; + case 'K': // hour in am/pm (0..11) + rule = selectNumberRule(Calendar.HOUR, tokenLen); + break; + case 'X': // ISO 8601 + rule = Iso8601_Rule.getRule(tokenLen); + break; + case 'z': // time zone (text) + if (tokenLen >= 4) { + rule = new TimeZoneNameRule(timeZone, locale, TimeZone.LONG); + } else { + rule = new TimeZoneNameRule(timeZone, locale, TimeZone.SHORT); + } + break; + case 'Z': // time zone (value) + if (tokenLen == 1) { + rule = TimeZoneNumberRule.INSTANCE_NO_COLON; + } else if (tokenLen == 2) { + rule = Iso8601_Rule.ISO8601_HOURS_COLON_MINUTES; + } else { + rule = TimeZoneNumberRule.INSTANCE_COLON; + } + break; + case '\'': // literal text + final String sub = token.substring(1); + if (sub.length() == 1) { + rule = new CharacterLiteral(sub.charAt(0)); + } else { + rule = new StringLiteral(sub); + } + break; + default: + throw new IllegalArgumentException("Illegal pattern component: " + token); + } + + rules.add(rule); + } + + return rules; + } + + /** + *

+ * Performs the parsing of tokens. + *

+ * + * @param pattern the pattern + * @param indexRef index references + * @return parsed token + */ + protected String parseToken(final String pattern, final int[] indexRef) { + final StringBuilder buf = new StringBuilder(); + + int i = indexRef[0]; + final int length = pattern.length(); + + char c = pattern.charAt(i); + if (c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z') { + // Scan a run of the same character, which indicates a time + // pattern. + buf.append(c); + + while (i + 1 < length) { + final char peek = pattern.charAt(i + 1); + if (peek == c) { + buf.append(c); + i++; + } else { + break; + } + } + } else { + // This will identify token as text. + buf.append('\''); + + boolean inLiteral = false; + + for (; i < length; i++) { + c = pattern.charAt(i); + + if (c == '\'') { + if (i + 1 < length && pattern.charAt(i + 1) == '\'') { + // '' is treated as escaped ' + i++; + buf.append(c); + } else { + inLiteral = !inLiteral; + } + } else if (!inLiteral && (c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z')) { + i--; + break; + } else { + buf.append(c); + } + } + } + + indexRef[0] = i; + return buf.toString(); + } + + /** + *

+ * Gets an appropriate rule for the padding required. + *

+ * + * @param field the field to get a rule for + * @param padding the padding required + * @return a new rule with the correct padding + */ + protected NumberRule selectNumberRule(final int field, final int padding) { + switch (padding) { + case 1: + return new UnpaddedNumberField(field); + case 2: + return new TwoDigitNumberField(field); + default: + return new PaddedNumberField(field, padding); + } + } + + // Format methods + // ----------------------------------------------------------------------- + + /** + *

+ * Formats a {@code Date}, {@code Calendar} or {@code Long} (milliseconds) object. + *

+ * + * @param obj the object to format + * @return The formatted value. + */ + String format(final Object obj) { + if (obj instanceof Date) { + return format((Date) obj); + } else if (obj instanceof Calendar) { + return format((Calendar) obj); + } else if (obj instanceof Long) { + return format(((Long) obj).longValue()); + } else { + throw new IllegalArgumentException("Unknown class: " + (obj == null ? "" : obj.getClass().getName())); + } + } + + @Override + public String format(final long millis) { + final Calendar c = Calendar.getInstance(timeZone, locale); + c.setTimeInMillis(millis); + return applyRulesToString(c); + } + + @Override + public String format(final Date date) { + final Calendar c = Calendar.getInstance(timeZone, locale); + c.setTime(date); + return applyRulesToString(c); + } + + @Override + public String format(final Calendar calendar) { + return format(calendar, new StringBuilder(mMaxLengthEstimate)).toString(); + } + + @Override + public B format(final long millis, final B buf) { + final Calendar c = Calendar.getInstance(timeZone, locale); + c.setTimeInMillis(millis); + return applyRules(c, buf); + } + + @Override + public B format(final Date date, final B buf) { + final Calendar c = Calendar.getInstance(timeZone, locale); + c.setTime(date); + return applyRules(c, buf); + } + + @Override + public B format(Calendar calendar, final B buf) { + // do not pass in calendar directly, this will cause TimeZone of FastDatePrinter to be ignored + if (!calendar.getTimeZone().equals(timeZone)) { + calendar = (Calendar) calendar.clone(); + calendar.setTimeZone(timeZone); + } + return applyRules(calendar, buf); + } + + /** + * Creates a String representation of the given Calendar by applying the rules of this printer to it. + * + * @param c the Calender to apply the rules to. + * @return a String representation of the given Calendar. + */ + private String applyRulesToString(final Calendar c) { + return applyRules(c, new StringBuilder(mMaxLengthEstimate)).toString(); + } + + /** + *

+ * Performs the formatting by applying the rules to the specified calendar. + *

+ * + * @param calendar the calendar to format + * @param buf the buffer to format into + * @param the Appendable class type, usually StringBuilder or StringBuffer. + * @return the specified string buffer + */ + private B applyRules(final Calendar calendar, final B buf) { + try { + for (final Rule rule : this.rules) { + rule.appendTo(buf, calendar); + } + } catch (final IOException e) { + throw new RuntimeException(e); + } + return buf; + } + + /** + *估算生成的日期字符串长度
+ * 实际生成的字符串长度小于或等于此值 + * + * @return 日期字符串长度 + */ + public int getMaxLengthEstimate() { + return mMaxLengthEstimate; + } + + // Serializing + // ----------------------------------------------------------------------- + /** + * Create the object after serialization. This implementation reinitializes the transient properties. + * + * @param in ObjectInputStream from which the object is being deserialized. + * @throws IOException if there is an IO issue. + * @throws ClassNotFoundException if a class cannot be found. + */ + private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException { + in.defaultReadObject(); + init(); + } + + /** + * Appends two digits to the given buffer. + * + * @param buffer the buffer to append to. + * @param value the value to append digits from. + */ + private static void appendDigits(final Appendable buffer, final int value) throws IOException { + buffer.append((char) (value / 10 + '0')); + buffer.append((char) (value % 10 + '0')); + } + + private static final int MAX_DIGITS = 10; // log10(Integer.MAX_VALUE) ~= 9.3 + + /** + * Appends all digits to the given buffer. + * + * @param buffer the buffer to append to. + * @param value the value to append digits from. + */ + private static void appendFullDigits(final Appendable buffer, int value, int minFieldWidth) throws IOException { + // specialized paths for 1 to 4 digits -> avoid the memory allocation from the temporary work array + // see LANG-1248 + if (value < 10000) { + // less memory allocation path works for four digits or less + + int nDigits = 4; + if (value < 1000) { + --nDigits; + if (value < 100) { + --nDigits; + if (value < 10) { + --nDigits; + } + } + } + // left zero pad + for (int i = minFieldWidth - nDigits; i > 0; --i) { + buffer.append('0'); + } + + switch (nDigits) { + case 4: + buffer.append((char) (value / 1000 + '0')); + value %= 1000; + case 3: + if (value >= 100) { + buffer.append((char) (value / 100 + '0')); + value %= 100; + } else { + buffer.append('0'); + } + case 2: + if (value >= 10) { + buffer.append((char) (value / 10 + '0')); + value %= 10; + } else { + buffer.append('0'); + } + case 1: + buffer.append((char) (value + '0')); + } + } else { + // more memory allocation path works for any digits + + // build up decimal representation in reverse + final char[] work = new char[MAX_DIGITS]; + int digit = 0; + while (value != 0) { + work[digit++] = (char) (value % 10 + '0'); + value = value / 10; + } + + // pad with zeros + while (digit < minFieldWidth) { + buffer.append('0'); + --minFieldWidth; + } + + // reverse + while (--digit >= 0) { + buffer.append(work[digit]); + } + } + } + + // Rules + // ----------------------------------------------------------------------- + /** + * 规则 + */ + private interface Rule { + /** + * Returns the estimated length of the result. + * + * @return the estimated length + */ + int estimateLength(); + + /** + * Appends the value of the specified calendar to the output buffer based on the rule implementation. + * + * @param buf the output buffer + * @param calendar calendar to be appended + * @throws IOException if an I/O error occurs + */ + void appendTo(Appendable buf, Calendar calendar) throws IOException; + } + + /** + *

+ * Inner class defining a numeric rule. + *

+ */ + private interface NumberRule extends Rule { + /** + * Appends the specified value to the output buffer based on the rule implementation. + * + * @param buffer the output buffer + * @param value the value to be appended + * @throws IOException if an I/O error occurs + */ + void appendTo(Appendable buffer, int value) throws IOException; + } + + /** + *

+ * Inner class to output a constant single character. + *

+ */ + private static class CharacterLiteral implements Rule { + private final char mValue; + + /** + * Constructs a new instance of {@code CharacterLiteral} to hold the specified value. + * + * @param value the character literal + */ + CharacterLiteral(final char value) { + mValue = value; + } + + @Override + public int estimateLength() { + return 1; + } + + @Override + public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException { + buffer.append(mValue); + } + } + + /** + *

+ * Inner class to output a constant string. + *

+ */ + private static class StringLiteral implements Rule { + private final String mValue; + + /** + * Constructs a new instance of {@code StringLiteral} to hold the specified value. + * + * @param value the string literal + */ + StringLiteral(final String value) { + mValue = value; + } + + /** + * {@inheritDoc} + */ + @Override + public int estimateLength() { + return mValue.length(); + } + + /** + * {@inheritDoc} + */ + @Override + public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException { + buffer.append(mValue); + } + } + + /** + *

+ * Inner class to output one of a set of values. + *

+ */ + private static class TextField implements Rule { + private final int mField; + private final String[] mValues; + + /** + * Constructs an instance of {@code TextField} with the specified field and values. + * + * @param field the field + * @param values the field values + */ + TextField(final int field, final String[] values) { + mField = field; + mValues = values; + } + + /** + * {@inheritDoc} + */ + @Override + public int estimateLength() { + int max = 0; + for (int i = mValues.length; --i >= 0;) { + final int len = mValues[i].length(); + if (len > max) { + max = len; + } + } + return max; + } + + /** + * {@inheritDoc} + */ + @Override + public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException { + buffer.append(mValues[calendar.get(mField)]); + } + } + + /** + *

+ * Inner class to output an unpadded number. + *

+ */ + private static class UnpaddedNumberField implements NumberRule { + private final int mField; + + /** + * Constructs an instance of {@code UnpadedNumberField} with the specified field. + * + * @param field the field + */ + UnpaddedNumberField(final int field) { + mField = field; + } + + /** + * {@inheritDoc} + */ + @Override + public int estimateLength() { + return 4; + } + + /** + * {@inheritDoc} + */ + @Override + public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException { + appendTo(buffer, calendar.get(mField)); + } + + /** + * {@inheritDoc} + */ + @Override + public final void appendTo(final Appendable buffer, final int value) throws IOException { + if (value < 10) { + buffer.append((char) (value + '0')); + } else if (value < 100) { + appendDigits(buffer, value); + } else { + appendFullDigits(buffer, value, 1); + } + } + } + + /** + *

+ * Inner class to output an unpadded month. + *

+ */ + private static class UnpaddedMonthField implements NumberRule { + static final UnpaddedMonthField INSTANCE = new UnpaddedMonthField(); + + /** + * Constructs an instance of {@code UnpaddedMonthField}. + * + */ + UnpaddedMonthField() { + super(); + } + + /** + * {@inheritDoc} + */ + @Override + public int estimateLength() { + return 2; + } + + /** + * {@inheritDoc} + */ + @Override + public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException { + appendTo(buffer, calendar.get(Calendar.MONTH) + 1); + } + + /** + * {@inheritDoc} + */ + @Override + public final void appendTo(final Appendable buffer, final int value) throws IOException { + if (value < 10) { + buffer.append((char) (value + '0')); + } else { + appendDigits(buffer, value); + } + } + } + + /** + *

+ * Inner class to output a padded number. + *

+ */ + private static class PaddedNumberField implements NumberRule { + private final int mField; + private final int mSize; + + /** + * Constructs an instance of {@code PaddedNumberField}. + * + * @param field the field + * @param size size of the output field + */ + PaddedNumberField(final int field, final int size) { + if (size < 3) { + // Should use UnpaddedNumberField or TwoDigitNumberField. + throw new IllegalArgumentException(); + } + mField = field; + mSize = size; + } + + /** + * {@inheritDoc} + */ + @Override + public int estimateLength() { + return mSize; + } + + /** + * {@inheritDoc} + */ + @Override + public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException { + appendTo(buffer, calendar.get(mField)); + } + + /** + * {@inheritDoc} + */ + @Override + public final void appendTo(final Appendable buffer, final int value) throws IOException { + appendFullDigits(buffer, value, mSize); + } + } + + /** + *

+ * Inner class to output a two digit number. + *

+ */ + private static class TwoDigitNumberField implements NumberRule { + private final int mField; + + /** + * Constructs an instance of {@code TwoDigitNumberField} with the specified field. + * + * @param field the field + */ + TwoDigitNumberField(final int field) { + mField = field; + } + + /** + * {@inheritDoc} + */ + @Override + public int estimateLength() { + return 2; + } + + /** + * {@inheritDoc} + */ + @Override + public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException { + appendTo(buffer, calendar.get(mField)); + } + + /** + * {@inheritDoc} + */ + @Override + public final void appendTo(final Appendable buffer, final int value) throws IOException { + if (value < 100) { + appendDigits(buffer, value); + } else { + appendFullDigits(buffer, value, 2); + } + } + } + + /** + *

+ * Inner class to output a two digit year. + *

+ */ + private static class TwoDigitYearField implements NumberRule { + static final TwoDigitYearField INSTANCE = new TwoDigitYearField(); + + /** + * Constructs an instance of {@code TwoDigitYearField}. + */ + TwoDigitYearField() { + super(); + } + + /** + * {@inheritDoc} + */ + @Override + public int estimateLength() { + return 2; + } + + /** + * {@inheritDoc} + */ + @Override + public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException { + appendTo(buffer, calendar.get(Calendar.YEAR) % 100); + } + + /** + * {@inheritDoc} + */ + @Override + public final void appendTo(final Appendable buffer, final int value) throws IOException { + appendDigits(buffer, value); + } + } + + /** + *

+ * Inner class to output a two digit month. + *

+ */ + private static class TwoDigitMonthField implements NumberRule { + static final TwoDigitMonthField INSTANCE = new TwoDigitMonthField(); + + /** + * Constructs an instance of {@code TwoDigitMonthField}. + */ + TwoDigitMonthField() { + super(); + } + + /** + * {@inheritDoc} + */ + @Override + public int estimateLength() { + return 2; + } + + /** + * {@inheritDoc} + */ + @Override + public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException { + appendTo(buffer, calendar.get(Calendar.MONTH) + 1); + } + + /** + * {@inheritDoc} + */ + @Override + public final void appendTo(final Appendable buffer, final int value) throws IOException { + appendDigits(buffer, value); + } + } + + /** + *

+ * Inner class to output the twelve hour field. + *

+ */ + private static class TwelveHourField implements NumberRule { + private final NumberRule mRule; + + /** + * Constructs an instance of {@code TwelveHourField} with the specified {@code NumberRule}. + * + * @param rule the rule + */ + TwelveHourField(final NumberRule rule) { + mRule = rule; + } + + /** + * {@inheritDoc} + */ + @Override + public int estimateLength() { + return mRule.estimateLength(); + } + + /** + * {@inheritDoc} + */ + @Override + public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException { + int value = calendar.get(Calendar.HOUR); + if (value == 0) { + value = calendar.getLeastMaximum(Calendar.HOUR) + 1; + } + mRule.appendTo(buffer, value); + } + + /** + * {@inheritDoc} + */ + @Override + public void appendTo(final Appendable buffer, final int value) throws IOException { + mRule.appendTo(buffer, value); + } + } + + /** + *

+ * Inner class to output the twenty four hour field. + *

+ */ + private static class TwentyFourHourField implements NumberRule { + private final NumberRule mRule; + + /** + * Constructs an instance of {@code TwentyFourHourField} with the specified {@code NumberRule}. + * + * @param rule the rule + */ + TwentyFourHourField(final NumberRule rule) { + mRule = rule; + } + + /** + * {@inheritDoc} + */ + @Override + public int estimateLength() { + return mRule.estimateLength(); + } + + /** + * {@inheritDoc} + */ + @Override + public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException { + int value = calendar.get(Calendar.HOUR_OF_DAY); + if (value == 0) { + value = calendar.getMaximum(Calendar.HOUR_OF_DAY) + 1; + } + mRule.appendTo(buffer, value); + } + + /** + * {@inheritDoc} + */ + @Override + public void appendTo(final Appendable buffer, final int value) throws IOException { + mRule.appendTo(buffer, value); + } + } + + /** + *

+ * Inner class to output the numeric day in week. + *

+ */ + private static class DayInWeekField implements NumberRule { + private final NumberRule mRule; + + DayInWeekField(final NumberRule rule) { + mRule = rule; + } + + @Override + public int estimateLength() { + return mRule.estimateLength(); + } + + @Override + public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException { + final int value = calendar.get(Calendar.DAY_OF_WEEK); + mRule.appendTo(buffer, value != Calendar.SUNDAY ? value - 1 : 7); + } + + @Override + public void appendTo(final Appendable buffer, final int value) throws IOException { + mRule.appendTo(buffer, value); + } + } + + /** + *

+ * Inner class to output the numeric day in week. + *

+ */ + private static class WeekYear implements NumberRule { + private final NumberRule mRule; + + WeekYear(final NumberRule rule) { + mRule = rule; + } + + @Override + public int estimateLength() { + return mRule.estimateLength(); + } + + @Override + public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException { + mRule.appendTo(buffer, calendar.getWeekYear()); + } + + @Override + public void appendTo(final Appendable buffer, final int value) throws IOException { + mRule.appendTo(buffer, value); + } + } + + // ----------------------------------------------------------------------- + + private static final ConcurrentMap cTimeZoneDisplayCache = new ConcurrentHashMap<>(7); + + /** + *

+ * Gets the time zone display name, using a cache for performance. + *

+ * + * @param tz the zone to query + * @param daylight true if daylight savings + * @param style the style to use {@code TimeZone.LONG} or {@code TimeZone.SHORT} + * @param locale the locale to use + * @return the textual name of the time zone + */ + static String getTimeZoneDisplay(final TimeZone tz, final boolean daylight, final int style, final Locale locale) { + final TimeZoneDisplayKey key = new TimeZoneDisplayKey(tz, daylight, style, locale); + String value = cTimeZoneDisplayCache.get(key); + if (value == null) { + // This is a very slow call, so cache the results. + value = tz.getDisplayName(daylight, style, locale); + final String prior = cTimeZoneDisplayCache.putIfAbsent(key, value); + if (prior != null) { + value = prior; + } + } + return value; + } + + /** + *

+ * Inner class to output a time zone name. + *

+ */ + private static class TimeZoneNameRule implements Rule { + private final Locale mLocale; + private final int mStyle; + private final String mStandard; + private final String mDaylight; + + /** + * Constructs an instance of {@code TimeZoneNameRule} with the specified properties. + * + * @param timeZone the time zone + * @param locale the locale + * @param style the style + */ + TimeZoneNameRule(final TimeZone timeZone, final Locale locale, final int style) { + mLocale = locale; + mStyle = style; + + mStandard = getTimeZoneDisplay(timeZone, false, style, locale); + mDaylight = getTimeZoneDisplay(timeZone, true, style, locale); + } + + /** + * {@inheritDoc} + */ + @Override + public int estimateLength() { + // We have no access to the Calendar object that will be passed to + // appendTo so base estimate on the TimeZone passed to the + // constructor + return Math.max(mStandard.length(), mDaylight.length()); + } + + /** + * {@inheritDoc} + */ + @Override + public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException { + final TimeZone zone = calendar.getTimeZone(); + if (calendar.get(Calendar.DST_OFFSET) != 0) { + buffer.append(getTimeZoneDisplay(zone, true, mStyle, mLocale)); + } else { + buffer.append(getTimeZoneDisplay(zone, false, mStyle, mLocale)); + } + } + } + + /** + *

+ * Inner class to output a time zone as a number {@code +/-HHMM} or {@code +/-HH:MM}. + *

+ */ + private static class TimeZoneNumberRule implements Rule { + static final TimeZoneNumberRule INSTANCE_COLON = new TimeZoneNumberRule(true); + static final TimeZoneNumberRule INSTANCE_NO_COLON = new TimeZoneNumberRule(false); + + final boolean mColon; + + /** + * Constructs an instance of {@code TimeZoneNumberRule} with the specified properties. + * + * @param colon add colon between HH and MM in the output if {@code true} + */ + TimeZoneNumberRule(final boolean colon) { + mColon = colon; + } + + /** + * {@inheritDoc} + */ + @Override + public int estimateLength() { + return 5; + } + + /** + * {@inheritDoc} + */ + @Override + public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException { + + int offset = calendar.get(Calendar.ZONE_OFFSET) + calendar.get(Calendar.DST_OFFSET); + + if (offset < 0) { + buffer.append('-'); + offset = -offset; + } else { + buffer.append('+'); + } + + final int hours = offset / (60 * 60 * 1000); + appendDigits(buffer, hours); + + if (mColon) { + buffer.append(':'); + } + + final int minutes = offset / (60 * 1000) - 60 * hours; + appendDigits(buffer, minutes); + } + } + + /** + *

+ * Inner class to output a time zone as a number {@code +/-HHMM} or {@code +/-HH:MM}. + *

+ */ + private static class Iso8601_Rule implements Rule { + + // Sign TwoDigitHours or Z + static final Iso8601_Rule ISO8601_HOURS = new Iso8601_Rule(3); + // Sign TwoDigitHours Minutes or Z + static final Iso8601_Rule ISO8601_HOURS_MINUTES = new Iso8601_Rule(5); + // Sign TwoDigitHours : Minutes or Z + static final Iso8601_Rule ISO8601_HOURS_COLON_MINUTES = new Iso8601_Rule(6); + + /** + * Factory method for Iso8601_Rules. + * + * @param tokenLen a token indicating the length of the TimeZone String to be formatted. + * @return a Iso8601_Rule that can format TimeZone String of length {@code tokenLen}. If no such rule exists, an IllegalArgumentException will be thrown. + */ + static Iso8601_Rule getRule(final int tokenLen) { + switch (tokenLen) { + case 1: + return Iso8601_Rule.ISO8601_HOURS; + case 2: + return Iso8601_Rule.ISO8601_HOURS_MINUTES; + case 3: + return Iso8601_Rule.ISO8601_HOURS_COLON_MINUTES; + default: + throw new IllegalArgumentException("invalid number of X"); + } + } + + final int length; + + /** + * Constructs an instance of {@code Iso8601_Rule} with the specified properties. + * + * @param length The number of characters in output (unless Z is output) + */ + Iso8601_Rule(final int length) { + this.length = length; + } + + /** + * {@inheritDoc} + */ + @Override + public int estimateLength() { + return length; + } + + /** + * {@inheritDoc} + */ + @Override + public void appendTo(final Appendable buffer, final Calendar calendar) throws IOException { + int offset = calendar.get(Calendar.ZONE_OFFSET) + calendar.get(Calendar.DST_OFFSET); + if (offset == 0) { + buffer.append("Z"); + return; + } + + if (offset < 0) { + buffer.append('-'); + offset = -offset; + } else { + buffer.append('+'); + } + + final int hours = offset / (60 * 60 * 1000); + appendDigits(buffer, hours); + + if (length < 5) { + return; + } + + if (length == 6) { + buffer.append(':'); + } + + final int minutes = offset / (60 * 1000) - 60 * hours; + appendDigits(buffer, minutes); + } + } + + // ---------------------------------------------------------------------- + /** + *

+ * Inner class that acts as a compound key for time zone names. + *

+ */ + private static class TimeZoneDisplayKey { + private final TimeZone mTimeZone; + private final int mStyle; + private final Locale mLocale; + + /** + * Constructs an instance of {@code TimeZoneDisplayKey} with the specified properties. + * + * @param timeZone the time zone + * @param daylight adjust the style for daylight saving time if {@code true} + * @param style the timezone style + * @param locale the timezone locale + */ + TimeZoneDisplayKey(final TimeZone timeZone, final boolean daylight, final int style, final Locale locale) { + mTimeZone = timeZone; + if (daylight) { + mStyle = style | 0x80000000; + } else { + mStyle = style; + } + mLocale = locale; + } + + /** + * {@inheritDoc} + */ + @Override + public int hashCode() { + return (mStyle * 31 + mLocale.hashCode()) * 31 + mTimeZone.hashCode(); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof TimeZoneDisplayKey) { + final TimeZoneDisplayKey other = (TimeZoneDisplayKey) obj; + return mTimeZone.equals(other.mTimeZone) && mStyle == other.mStyle && mLocale.equals(other.mLocale); + } + return false; + } + } +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/date/format/FormatCache.java b/blade-core-tool/src/main/java/org/springblade/core/tool/date/format/FormatCache.java new file mode 100644 index 0000000..0d8924a --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/date/format/FormatCache.java @@ -0,0 +1,252 @@ +package org.springblade.core.tool.date.format; + +import java.text.DateFormat; +import java.text.Format; +import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.Locale; +import java.util.TimeZone; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * 日期格式化器缓存
+ * Thanks to Apache Commons Lang 3.5 + * + * @since 2.16.2 + */ +abstract class FormatCache { + + /** + * No date or no time. Used in same parameters as DateFormat.SHORT or DateFormat.LONG + */ + static final int NONE = -1; + + private final ConcurrentMap cInstanceCache = new ConcurrentHashMap<>(7); + + private static final ConcurrentMap cDateTimeInstanceCache = new ConcurrentHashMap<>(7); + + /** + * 使用默认的pattern、timezone和locale获得缓存中的实例 + * @return a date/time formatter + */ + public F getInstance() { + return getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, TimeZone.getDefault(), Locale.getDefault()); + } + + /** + * 使用 pattern, time zone and locale 获得对应的 格式化器 + * + * @param pattern 非空日期格式,使用与 {@link SimpleDateFormat}相同格式 + * @param timeZone 时区,默认当前时区 + * @param locale 地区,默认使用当前地区 + * @return 格式化器 + * @throws IllegalArgumentException pattern 无效或null + */ + public F getInstance(final String pattern, TimeZone timeZone, Locale locale) { + if (pattern == null) { + throw new NullPointerException("pattern must not be null"); + } + if (timeZone == null) { + timeZone = TimeZone.getDefault(); + } + if (locale == null) { + locale = Locale.getDefault(); + } + final MultipartKey key = new MultipartKey(pattern, timeZone, locale); + F format = cInstanceCache.get(key); + if (format == null) { + format = createInstance(pattern, timeZone, locale); + final F previousValue = cInstanceCache.putIfAbsent(key, format); + if (previousValue != null) { + // another thread snuck in and did the same work + // we should return the instance that is in ConcurrentMap + format = previousValue; + } + } + return format; + } + + /** + * 创建格式化器 + * + * @param pattern 非空日期格式,使用与 {@link SimpleDateFormat}相同格式 + * @param timeZone 时区,默认当前时区 + * @param locale 地区,默认使用当前地区 + * @return 格式化器 + * @throws IllegalArgumentException pattern 无效或null + */ + abstract protected F createInstance(String pattern, TimeZone timeZone, Locale locale); + + /** + *

+ * Gets a date/time formatter instance using the specified style, time zone and locale. + *

+ * + * @param dateStyle date style: FULL, LONG, MEDIUM, or SHORT, null indicates no date in format + * @param timeStyle time style: FULL, LONG, MEDIUM, or SHORT, null indicates no time in format + * @param timeZone optional time zone, overrides time zone of formatted date, null means use default Locale + * @param locale optional locale, overrides system locale + * @return a localized standard date/time formatter + * @throws IllegalArgumentException if the Locale has no date/time pattern defined + */ + // This must remain private, see LANG-884 + private F getDateTimeInstance(final Integer dateStyle, final Integer timeStyle, final TimeZone timeZone, Locale locale) { + if (locale == null) { + locale = Locale.getDefault(); + } + final String pattern = getPatternForStyle(dateStyle, timeStyle, locale); + return getInstance(pattern, timeZone, locale); + } + + /** + *

+ * Gets a date/time formatter instance using the specified style, time zone and locale. + *

+ * + * @param dateStyle date style: FULL, LONG, MEDIUM, or SHORT + * @param timeStyle time style: FULL, LONG, MEDIUM, or SHORT + * @param timeZone optional time zone, overrides time zone of formatted date, null means use default Locale + * @param locale optional locale, overrides system locale + * @return a localized standard date/time formatter + * @throws IllegalArgumentException if the Locale has no date/time pattern defined + */ + // package protected, for access from FastDateFormat; do not make public or protected + F getDateTimeInstance(final int dateStyle, final int timeStyle, final TimeZone timeZone, final Locale locale) { + return getDateTimeInstance(Integer.valueOf(dateStyle), Integer.valueOf(timeStyle), timeZone, locale); + } + + /** + *

+ * Gets a date formatter instance using the specified style, time zone and locale. + *

+ * + * @param dateStyle date style: FULL, LONG, MEDIUM, or SHORT + * @param timeZone optional time zone, overrides time zone of formatted date, null means use default Locale + * @param locale optional locale, overrides system locale + * @return a localized standard date/time formatter + * @throws IllegalArgumentException if the Locale has no date/time pattern defined + */ + // package protected, for access from FastDateFormat; do not make public or protected + F getDateInstance(final int dateStyle, final TimeZone timeZone, final Locale locale) { + return getDateTimeInstance(Integer.valueOf(dateStyle), null, timeZone, locale); + } + + /** + *

+ * Gets a time formatter instance using the specified style, time zone and locale. + *

+ * + * @param timeStyle time style: FULL, LONG, MEDIUM, or SHORT + * @param timeZone optional time zone, overrides time zone of formatted date, null means use default Locale + * @param locale optional locale, overrides system locale + * @return a localized standard date/time formatter + * @throws IllegalArgumentException if the Locale has no date/time pattern defined + */ + // package protected, for access from FastDateFormat; do not make public or protected + F getTimeInstance(final int timeStyle, final TimeZone timeZone, final Locale locale) { + return getDateTimeInstance(null, Integer.valueOf(timeStyle), timeZone, locale); + } + + /** + *

+ * Gets a date/time format for the specified styles and locale. + *

+ * + * @param dateStyle date style: FULL, LONG, MEDIUM, or SHORT, null indicates no date in format + * @param timeStyle time style: FULL, LONG, MEDIUM, or SHORT, null indicates no time in format + * @param locale The non-null locale of the desired format + * @return a localized standard date/time format + * @throws IllegalArgumentException if the Locale has no date/time pattern defined + */ + // package protected, for access from test code; do not make public or protected + static String getPatternForStyle(final Integer dateStyle, final Integer timeStyle, final Locale locale) { + final MultipartKey key = new MultipartKey(dateStyle, timeStyle, locale); + + String pattern = cDateTimeInstanceCache.get(key); + if (pattern == null) { + try { + DateFormat formatter; + if (dateStyle == null) { + formatter = DateFormat.getTimeInstance(timeStyle.intValue(), locale); + } else if (timeStyle == null) { + formatter = DateFormat.getDateInstance(dateStyle.intValue(), locale); + } else { + formatter = DateFormat.getDateTimeInstance(dateStyle.intValue(), timeStyle.intValue(), locale); + } + pattern = ((SimpleDateFormat) formatter).toPattern(); + final String previous = cDateTimeInstanceCache.putIfAbsent(key, pattern); + if (previous != null) { + // even though it doesn't matter if another thread put the pattern + // it's still good practice to return the String instance that is + // actually in the ConcurrentMap + pattern = previous; + } + } catch (final ClassCastException ex) { + throw new IllegalArgumentException("No date time pattern for locale: " + locale); + } + } + return pattern; + } + + // ---------------------------------------------------------------------- + /** + *

+ * Helper class to hold multi-part Map keys + *

+ */ + private static class MultipartKey { + private final Object[] keys; + private int hashCode; + + /** + * Constructs an instance of MultipartKey to hold the specified objects. + * + * @param keys the set of objects that make up the key. Each key may be null. + */ + public MultipartKey(final Object... keys) { + this.keys = keys; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final MultipartKey other = (MultipartKey) obj; + if (false == Arrays.equals(keys, other.keys)) { + return false; + } + return true; + } + + + + /** + * {@inheritDoc} + */ + @Override + public int hashCode() { + if (hashCode == 0) { + int rc = 0; + for (final Object key : keys) { + if (key != null) { + rc = rc * 7 + key.hashCode(); + } + } + hashCode = rc; + } + return hashCode; + } + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/jackson/AbstractReadWriteJackson2HttpMessageConverter.java b/blade-core-tool/src/main/java/org/springblade/core/tool/jackson/AbstractReadWriteJackson2HttpMessageConverter.java new file mode 100644 index 0000000..eb62cbb --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/jackson/AbstractReadWriteJackson2HttpMessageConverter.java @@ -0,0 +1,141 @@ +/** + * Copyright (c) 2018-2028, DreamLu 卢春梦 (qq596392912@gmail.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.tool.jackson; + +import com.fasterxml.jackson.core.JsonEncoding; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.PrettyPrinter; +import com.fasterxml.jackson.core.util.DefaultIndenter; +import com.fasterxml.jackson.core.util.DefaultPrettyPrinter; +import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.exc.InvalidDefinitionException; +import com.fasterxml.jackson.databind.ser.FilterProvider; +import org.springblade.core.tool.utils.Charsets; +import org.springframework.http.HttpOutputMessage; +import org.springframework.http.MediaType; +import org.springframework.http.converter.HttpMessageConversionException; +import org.springframework.http.converter.HttpMessageNotWritableException; +import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter; +import org.springframework.http.converter.json.MappingJacksonValue; +import org.springframework.lang.Nullable; +import org.springframework.util.TypeUtils; + +import java.io.IOException; +import java.lang.reflect.Type; +import java.util.Arrays; +import java.util.Collections; +import java.util.concurrent.atomic.AtomicReference; + +/** + * 分读写的 json 消息 处理器 + * + * @author L.cm + */ +public abstract class AbstractReadWriteJackson2HttpMessageConverter extends AbstractJackson2HttpMessageConverter { + private static final java.nio.charset.Charset DEFAULT_CHARSET = Charsets.UTF_8; + + private ObjectMapper writeObjectMapper; + @Nullable + private PrettyPrinter ssePrettyPrinter; + + public AbstractReadWriteJackson2HttpMessageConverter(ObjectMapper readObjectMapper, ObjectMapper writeObjectMapper) { + super(readObjectMapper); + this.writeObjectMapper = writeObjectMapper; + initSsePrettyPrinter(); + } + + public AbstractReadWriteJackson2HttpMessageConverter(ObjectMapper readObjectMapper, ObjectMapper writeObjectMapper, MediaType supportedMediaType) { + this(readObjectMapper, writeObjectMapper); + setSupportedMediaTypes(Collections.singletonList(supportedMediaType)); + initSsePrettyPrinter(); + } + + public AbstractReadWriteJackson2HttpMessageConverter(ObjectMapper readObjectMapper, ObjectMapper writeObjectMapper, MediaType... supportedMediaTypes) { + this(readObjectMapper, writeObjectMapper); + setSupportedMediaTypes(Arrays.asList(supportedMediaTypes)); + } + + private void initSsePrettyPrinter() { + setDefaultCharset(DEFAULT_CHARSET); + DefaultPrettyPrinter prettyPrinter = new DefaultPrettyPrinter(); + prettyPrinter.indentObjectsWith(new DefaultIndenter(" ", "\ndata:")); + this.ssePrettyPrinter = prettyPrinter; + } + + @Override + public boolean canWrite(Class clazz, @Nullable MediaType mediaType) { + if (!canWrite(mediaType)) { + return false; + } + AtomicReference causeRef = new AtomicReference<>(); + if (this.objectMapper.canSerialize(clazz, causeRef)) { + return true; + } + logWarningIfNecessary(clazz, causeRef.get()); + return false; + } + + @Override + protected void writeInternal(Object object, @Nullable Type type, HttpOutputMessage outputMessage) + throws IOException, HttpMessageNotWritableException { + + MediaType contentType = outputMessage.getHeaders().getContentType(); + JsonEncoding encoding = getJsonEncoding(contentType); + JsonGenerator generator = this.writeObjectMapper.getFactory().createGenerator(outputMessage.getBody(), encoding); + try { + writePrefix(generator, object); + + Object value = object; + Class serializationView = null; + FilterProvider filters = null; + JavaType javaType = null; + + if (object instanceof MappingJacksonValue) { + MappingJacksonValue container = (MappingJacksonValue) object; + value = container.getValue(); + serializationView = container.getSerializationView(); + filters = container.getFilters(); + } + if (type != null && TypeUtils.isAssignable(type, value.getClass())) { + javaType = getJavaType(type, null); + } + + ObjectWriter objectWriter = (serializationView != null ? + this.writeObjectMapper.writerWithView(serializationView) : this.writeObjectMapper.writer()); + if (filters != null) { + objectWriter = objectWriter.with(filters); + } + if (javaType != null && javaType.isContainerType()) { + objectWriter = objectWriter.forType(javaType); + } + SerializationConfig config = objectWriter.getConfig(); + if (contentType != null && contentType.isCompatibleWith(MediaType.TEXT_EVENT_STREAM) && + config.isEnabled(SerializationFeature.INDENT_OUTPUT)) { + objectWriter = objectWriter.with(this.ssePrettyPrinter); + } + objectWriter.writeValue(generator, value); + + writeSuffix(generator, object); + generator.flush(); + } catch (InvalidDefinitionException ex) { + throw new HttpMessageConversionException("Type definition error: " + ex.getType(), ex); + } catch (JsonProcessingException ex) { + throw new HttpMessageNotWritableException("Could not write JSON: " + ex.getOriginalMessage(), ex); + } + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/jackson/BladeBeanSerializerModifier.java b/blade-core-tool/src/main/java/org/springblade/core/tool/jackson/BladeBeanSerializerModifier.java new file mode 100644 index 0000000..782baf1 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/jackson/BladeBeanSerializerModifier.java @@ -0,0 +1,122 @@ +/** + * Copyright (c) 2018-2028, DreamLu 卢春梦 (qq596392912@gmail.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.tool.jackson; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.ser.BeanPropertyWriter; +import com.fasterxml.jackson.databind.ser.BeanSerializerModifier; +import org.springblade.core.tool.utils.StringUtil; +import org.springblade.core.tool.utils.StringPool; + +import java.io.IOException; +import java.time.OffsetDateTime; +import java.time.temporal.TemporalAccessor; +import java.util.Collection; +import java.util.Date; +import java.util.List; + +/** + * jackson 默认值为 null 时的处理 + * + * 主要是为了避免 app 端出现null导致闪退 + * + * 规则: + * number -1 + * string "" + * date "" + * boolean false + * array [] + * Object {} + * + * @author L.cm + */ +public class BladeBeanSerializerModifier extends BeanSerializerModifier { + @Override + public List changeProperties( + SerializationConfig config, BeanDescription beanDesc, + List beanProperties) { + // 循环所有的beanPropertyWriter + beanProperties.forEach(writer -> { + // 如果已经有 null 序列化处理如注解:@JsonSerialize(nullsUsing = xxx) 跳过 + if (writer.hasNullSerializer()) { + return; + } + JavaType type = writer.getType(); + Class clazz = type.getRawClass(); + if (type.isTypeOrSubTypeOf(Number.class)) { + writer.assignNullSerializer(NullJsonSerializers.NUMBER_JSON_SERIALIZER); + }else if (type.isTypeOrSubTypeOf(Boolean.class)) { + writer.assignNullSerializer(NullJsonSerializers.BOOLEAN_JSON_SERIALIZER); + } else if (type.isTypeOrSubTypeOf(Character.class)) { + writer.assignNullSerializer(NullJsonSerializers.STRING_JSON_SERIALIZER); + } else if (type.isTypeOrSubTypeOf(String.class)) { + writer.assignNullSerializer(NullJsonSerializers.STRING_JSON_SERIALIZER); + } else if (type.isArrayType() || clazz.isArray() || type.isTypeOrSubTypeOf(Collection.class)) { + writer.assignNullSerializer(NullJsonSerializers.ARRAY_JSON_SERIALIZER); + } else if (type.isTypeOrSubTypeOf(OffsetDateTime.class)) { + writer.assignNullSerializer(NullJsonSerializers.STRING_JSON_SERIALIZER); + } else if (type.isTypeOrSubTypeOf(Date.class) || type.isTypeOrSubTypeOf(TemporalAccessor.class)) { + writer.assignNullSerializer(NullJsonSerializers.STRING_JSON_SERIALIZER); + } else { + writer.assignNullSerializer(NullJsonSerializers.OBJECT_JSON_SERIALIZER); + } + }); + return super.changeProperties(config, beanDesc, beanProperties); + } + + public interface NullJsonSerializers { + + JsonSerializer STRING_JSON_SERIALIZER = new JsonSerializer() { + @Override + public void serialize(Object value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + gen.writeString(StringPool.EMPTY); + } + }; + + JsonSerializer NUMBER_JSON_SERIALIZER = new JsonSerializer() { + @Override + public void serialize(Object value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + gen.writeNumber(StringUtil.INDEX_NOT_FOUND); + } + }; + + JsonSerializer BOOLEAN_JSON_SERIALIZER = new JsonSerializer() { + @Override + public void serialize(Object value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + gen.writeObject(Boolean.FALSE); + } + }; + + JsonSerializer ARRAY_JSON_SERIALIZER = new JsonSerializer() { + @Override + public void serialize(Object value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + gen.writeStartArray(); + gen.writeEndArray(); + } + }; + + JsonSerializer OBJECT_JSON_SERIALIZER = new JsonSerializer() { + @Override + public void serialize(Object value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + gen.writeStartObject(); + gen.writeEndObject(); + } + }; + + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/jackson/BladeJavaTimeModule.java b/blade-core-tool/src/main/java/org/springblade/core/tool/jackson/BladeJavaTimeModule.java new file mode 100644 index 0000000..d6d6b1a --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/jackson/BladeJavaTimeModule.java @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2018-2028, DreamLu 卢春梦 (qq596392912@gmail.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.tool.jackson; + +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.datatype.jsr310.PackageVersion; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer; +import org.springblade.core.tool.utils.DateTimeUtil; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; + +/** + * java 8 时间默认序列化 + * + * @author L.cm + */ +public class BladeJavaTimeModule extends SimpleModule { + + public BladeJavaTimeModule() { + super(PackageVersion.VERSION); + this.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeUtil.DATETIME_FORMAT)); + this.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeUtil.DATE_FORMAT)); + this.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeUtil.TIME_FORMAT)); + this.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeUtil.DATETIME_FORMAT)); + this.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeUtil.DATE_FORMAT)); + this.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeUtil.TIME_FORMAT)); + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/jackson/JsonUtil.java b/blade-core-tool/src/main/java/org/springblade/core/tool/jackson/JsonUtil.java new file mode 100644 index 0000000..9d39c20 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/jackson/JsonUtil.java @@ -0,0 +1,328 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.tool.jackson; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import lombok.extern.slf4j.Slf4j; +import org.springblade.core.tool.date.DatePattern; +import org.springblade.core.tool.utils.Exceptions; +import org.springblade.core.tool.utils.StringUtil; + +import java.io.IOException; +import java.io.InputStream; +import java.text.SimpleDateFormat; +import java.time.ZoneId; +import java.util.*; + +/** + * Jackson工具类 + */ +@Slf4j +public class JsonUtil { + + /** + * 将对象序列化成json字符串 + * + * @param value javaBean + * @return jsonString json字符串 + */ + public static String toJson(T value) { + try { + return getInstance().writeValueAsString(value); + } catch (Exception e) { + log.error(e.getMessage(), e); + } + return null; + } + + /** + * 将对象序列化成 json byte 数组 + * + * @param object javaBean + * @return jsonString json字符串 + */ + public static byte[] toJsonAsBytes(Object object) { + try { + return getInstance().writeValueAsBytes(object); + } catch (JsonProcessingException e) { + throw Exceptions.unchecked(e); + } + } + + /** + * 将json反序列化成对象 + * + * @param content content + * @param valueType class + * @param T 泛型标记 + * @return Bean + */ + public static T parse(String content, Class valueType) { + try { + return getInstance().readValue(content, valueType); + } catch (Exception e) { + log.error(e.getMessage(), e); + } + return null; + } + + /** + * 将json反序列化成对象 + * + * @param content content + * @param typeReference 泛型类型 + * @param T 泛型标记 + * @return Bean + */ + public static T parse(String content, TypeReference typeReference) { + try { + return getInstance().readValue(content, typeReference); + } catch (IOException e) { + throw Exceptions.unchecked(e); + } + } + + /** + * 将json byte 数组反序列化成对象 + * + * @param bytes json bytes + * @param valueType class + * @param T 泛型标记 + * @return Bean + */ + public static T parse(byte[] bytes, Class valueType) { + try { + return getInstance().readValue(bytes, valueType); + } catch (IOException e) { + throw Exceptions.unchecked(e); + } + } + + + /** + * 将json反序列化成对象 + * + * @param bytes bytes + * @param typeReference 泛型类型 + * @param T 泛型标记 + * @return Bean + */ + public static T parse(byte[] bytes, TypeReference typeReference) { + try { + return getInstance().readValue(bytes, typeReference); + } catch (IOException e) { + throw Exceptions.unchecked(e); + } + } + + /** + * 将json反序列化成对象 + * + * @param in InputStream + * @param valueType class + * @param T 泛型标记 + * @return Bean + */ + public static T parse(InputStream in, Class valueType) { + try { + return getInstance().readValue(in, valueType); + } catch (IOException e) { + throw Exceptions.unchecked(e); + } + } + + /** + * 将json反序列化成对象 + * + * @param in InputStream + * @param typeReference 泛型类型 + * @param T 泛型标记 + * @return Bean + */ + public static T parse(InputStream in, TypeReference typeReference) { + try { + return getInstance().readValue(in, typeReference); + } catch (IOException e) { + throw Exceptions.unchecked(e); + } + } + + /** + * 将json反序列化成List对象 + * @param content content + * @param valueTypeRef class + * @param T 泛型标记 + * @return + */ + public static List parseArray(String content, Class valueTypeRef) { + try { + + if (!StringUtil.startsWithIgnoreCase(content, "[")) { + content = "[" + content + "]"; + } + + List> list = getInstance().readValue(content, new TypeReference>() { + }); + List result = new ArrayList<>(); + for (Map map : list) { + result.add(toPojo(map, valueTypeRef)); + } + return result; + } catch (IOException e) { + log.error(e.getMessage(), e); + } + return null; + } + + public static Map toMap(String content) { + try { + return getInstance().readValue(content, Map.class); + } catch (IOException e) { + log.error(e.getMessage(), e); + } + return null; + } + + public static Map toMap(String content, Class valueTypeRef) { + try { + Map> map = getInstance().readValue(content, new TypeReference>() { + }); + Map result = new HashMap<>(); + for (Map.Entry> entry : map.entrySet()) { + result.put(entry.getKey(), toPojo(entry.getValue(), valueTypeRef)); + } + return result; + } catch (IOException e) { + log.error(e.getMessage(), e); + } + return null; + } + + public static T toPojo(Map fromValue, Class toValueType) { + return getInstance().convertValue(fromValue, toValueType); + } + + /** + * 将json字符串转成 JsonNode + * + * @param jsonString jsonString + * @return jsonString json字符串 + */ + public static JsonNode readTree(String jsonString) { + try { + return getInstance().readTree(jsonString); + } catch (IOException e) { + throw Exceptions.unchecked(e); + } + } + + /** + * 将json字符串转成 JsonNode + * + * @param in InputStream + * @return jsonString json字符串 + */ + public static JsonNode readTree(InputStream in) { + try { + return getInstance().readTree(in); + } catch (IOException e) { + throw Exceptions.unchecked(e); + } + } + + /** + * 将json字符串转成 JsonNode + * + * @param content content + * @return jsonString json字符串 + */ + public static JsonNode readTree(byte[] content) { + try { + return getInstance().readTree(content); + } catch (IOException e) { + throw Exceptions.unchecked(e); + } + } + + /** + * 将json字符串转成 JsonNode + * + * @param jsonParser JsonParser + * @return jsonString json字符串 + */ + public static JsonNode readTree(JsonParser jsonParser) { + try { + return getInstance().readTree(jsonParser); + } catch (IOException e) { + throw Exceptions.unchecked(e); + } + } + + public static ObjectMapper getInstance() { + return JacksonHolder.INSTANCE; + } + + private static class JacksonHolder { + private static ObjectMapper INSTANCE = new JacksonObjectMapper(); + } + + public static class JacksonObjectMapper extends ObjectMapper { + private static final long serialVersionUID = 4288193147502386170L; + + private static final Locale CHINA = Locale.CHINA; + + public JacksonObjectMapper() { + super(); + //设置地点为中国 + super.setLocale(CHINA); + //去掉默认的时间戳格式 + super.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); + //设置为中国上海时区 + super.setTimeZone(TimeZone.getTimeZone(ZoneId.systemDefault())); + //序列化时,日期的统一格式 + super.setDateFormat(new SimpleDateFormat(DatePattern.NORM_DATETIME_PATTERN, Locale.CHINA)); + //序列化处理 + super.configure(JsonParser.Feature.ALLOW_UNQUOTED_CONTROL_CHARS, true); + super.configure(JsonParser.Feature.ALLOW_BACKSLASH_ESCAPING_ANY_CHARACTER, true); + super.findAndRegisterModules(); + //失败处理 + super.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); + super.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + //单引号处理 + super.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true); + //反序列化时,属性不存在的兼容处理s + super.getDeserializationConfig().withoutFeatures(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + //序列化处理 + super.setSerializerFactory(this.getSerializerFactory().withSerializerModifier(new BladeBeanSerializerModifier())); + super.getSerializerProvider().setNullValueSerializer(BladeBeanSerializerModifier.NullJsonSerializers.STRING_JSON_SERIALIZER); + //日期格式化 + super.registerModule(new BladeJavaTimeModule()); + super.findAndRegisterModules(); + } + + @Override + public ObjectMapper copy() { + return super.copy(); + } + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/jackson/MappingApiJackson2HttpMessageConverter.java b/blade-core-tool/src/main/java/org/springblade/core/tool/jackson/MappingApiJackson2HttpMessageConverter.java new file mode 100644 index 0000000..8c6536b --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/jackson/MappingApiJackson2HttpMessageConverter.java @@ -0,0 +1,99 @@ +/** + * Copyright (c) 2018-2028, DreamLu 卢春梦 (qq596392912@gmail.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.tool.jackson; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.http.MediaType; +import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; +import org.springframework.lang.Nullable; + +import java.io.IOException; + +/** + * 针对 api 服务对 android 和 ios 和 web 处理的 分读写的 jackson 处理 + * + *

+ * 1. app 端上报数据是 使用 readObjectMapper + * 2. 返回给 app 端的数据使用 writeObjectMapper + *

+ * + * @author L.cm + */ +public class MappingApiJackson2HttpMessageConverter extends AbstractReadWriteJackson2HttpMessageConverter { + + @Nullable + private String jsonPrefix; + + /** + * Construct a new {@link MappingApiJackson2HttpMessageConverter} using default configuration + * provided by {@link Jackson2ObjectMapperBuilder}. + */ + public MappingApiJackson2HttpMessageConverter() { + this(Jackson2ObjectMapperBuilder.json().build()); + } + + /** + * Construct a new {@link MappingApiJackson2HttpMessageConverter} with a custom {@link ObjectMapper}. + * You can use {@link Jackson2ObjectMapperBuilder} to build it easily. + * @param objectMapper ObjectMapper + * @see Jackson2ObjectMapperBuilder#json() + */ + public MappingApiJackson2HttpMessageConverter(ObjectMapper objectMapper) { + super(objectMapper, initWriteObjectMapper(objectMapper), MediaType.APPLICATION_JSON, new MediaType("application", "*+json")); + } + + private static ObjectMapper initWriteObjectMapper(ObjectMapper readObjectMapper) { + // 拷贝 readObjectMapper + ObjectMapper writeObjectMapper = readObjectMapper.copy(); + // null 处理 + writeObjectMapper.setSerializerFactory(writeObjectMapper.getSerializerFactory().withSerializerModifier(new BladeBeanSerializerModifier())); + writeObjectMapper.getSerializerProvider().setNullValueSerializer(BladeBeanSerializerModifier.NullJsonSerializers.STRING_JSON_SERIALIZER); + return writeObjectMapper; + } + + /** + * Specify a custom prefix to use for this view's JSON output. + * Default is none. + * + * @param jsonPrefix jsonPrefix + * @see #setPrefixJson + */ + public void setJsonPrefix(String jsonPrefix) { + this.jsonPrefix = jsonPrefix; + } + + /** + * Indicate whether the JSON output by this view should be prefixed with ")]}', ". Default is false. + *

Prefixing the JSON string in this manner is used to help prevent JSON Hijacking. + * The prefix renders the string syntactically invalid as a script so that it cannot be hijacked. + * This prefix should be stripped before parsing the string as JSON. + * + * @param prefixJson prefixJson + * @see #setJsonPrefix + */ + public void setPrefixJson(boolean prefixJson) { + this.jsonPrefix = (prefixJson ? ")]}', " : null); + } + + @Override + protected void writePrefix(JsonGenerator generator, Object object) throws IOException { + if (this.jsonPrefix != null) { + generator.writeRaw(this.jsonPrefix); + } + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/support/BeanProperty.java b/blade-core-tool/src/main/java/org/springblade/core/tool/support/BeanProperty.java new file mode 100644 index 0000000..d8ecaa6 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/support/BeanProperty.java @@ -0,0 +1,14 @@ +package org.springblade.core.tool.support; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * Bean属性 + */ +@Getter +@AllArgsConstructor +public class BeanProperty { + private final String name; + private final Class type; +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/support/CMap.java b/blade-core-tool/src/main/java/org/springblade/core/tool/support/CMap.java new file mode 100644 index 0000000..911f567 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/support/CMap.java @@ -0,0 +1,198 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.tool.support; + +import org.springblade.core.tool.utils.Func; + +import java.sql.Time; +import java.sql.Timestamp; +import java.util.Date; +import java.util.HashMap; + +/** + * 链式map + */ +public class CMap extends CaseInsensitiveHashMap { + + + private CMap(){ + + } + + /** + * 创建CMap + * @return CMap + */ + public static CMap init() { + return new CMap(); + } + + public static HashMap newHashMap() { + return new HashMap(); + } + + /** + * 设置列 + * @param attr 属性 + * @param value 值 + * @return 本身 + */ + public CMap set(String attr, Object value) { + this.put(attr, value); + return this; + } + + /** + * 设置列,当键或值为null时忽略 + * @param attr 属性 + * @param value 值 + * @return 本身 + */ + public CMap setIgnoreNull(String attr, Object value) { + if(null != attr && null != value) { + set(attr, value); + } + return this; + } + + public Object getObj(String key) { + return super.get(key); + } + + /** + * 获得特定类型值 + * + * @param 值类型 + * @param attr 字段名 + * @param defaultValue 默认值 + * @return 字段值 + */ + public T get(String attr, T defaultValue) { + final Object result = get(attr); + return (T)(result != null ? result : defaultValue); + } + + /** + * 获得特定类型值 + * + * @param attr 字段名 + * @return 字段值 + */ + public String getStr(String attr) { + return Func.toStr(get(attr), null); + } + + /** + * 获得特定类型值 + * + * @param attr 字段名 + * @return 字段值 + */ + public Integer getInt(String attr) { + return Func.toInt(get(attr), -1); + } + + /** + * 获得特定类型值 + * + * @param attr 字段名 + * @return 字段值 + */ + public Long getLong(String attr) { + return Func.toLong(get(attr), -1l); + } + + /** + * 获得特定类型值 + * + * @param attr 字段名 + * @return 字段值 + */ + public Float getFloat(String attr) { + return Func.toFloat(get(attr), null); + } + + public Double getDouble(String attr) { + return Func.toDouble(get(attr), null); + } + + + /** + * 获得特定类型值 + * + * @param attr 字段名 + * @return 字段值 + */ + public Boolean getBool(String attr) { + return Func.toBoolean(get(attr), null); + } + + /** + * 获得特定类型值 + * + * @param attr 字段名 + * @return 字段值 + */ + public byte[] getBytes(String attr) { + return get(attr, null); + } + + /** + * 获得特定类型值 + * + * @param attr 字段名 + * @return 字段值 + */ + public Date getDate(String attr) { + return get(attr, null); + } + + /** + * 获得特定类型值 + * + * @param attr 字段名 + * @return 字段值 + */ + public Time getTime(String attr) { + return get(attr, null); + } + + /** + * 获得特定类型值 + * + * @param attr 字段名 + * @return 字段值 + */ + public Timestamp getTimestamp(String attr) { + return get(attr, null); + } + + /** + * 获得特定类型值 + * + * @param attr 字段名 + * @return 字段值 + */ + public Number getNumber(String attr) { + return get(attr, null); + } + + @Override + public CMap clone() { + return (CMap) super.clone(); + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/support/CaseInsensitiveHashMap.java b/blade-core-tool/src/main/java/org/springblade/core/tool/support/CaseInsensitiveHashMap.java new file mode 100644 index 0000000..e62b81d --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/support/CaseInsensitiveHashMap.java @@ -0,0 +1,68 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.tool.support; + +import java.util.*; + + +public class CaseInsensitiveHashMap extends LinkedHashMap { + + private static final long serialVersionUID = 9178606903603606031L; + + private final Map lowerCaseMap = new HashMap(); + + @Override + public boolean containsKey(Object key) { + Object realKey = lowerCaseMap.get(key.toString().toLowerCase(Locale.ENGLISH)); + return super.containsKey(realKey); + } + + @Override + public Object get(Object key) { + Object realKey = lowerCaseMap.get(key.toString().toLowerCase(Locale.ENGLISH)); + return super.get(realKey); + } + + @Override + public Set keySet() { + return lowerCaseMap.keySet(); + } + + @Override + public Object put(String key, Object value) { + Object oldKey = lowerCaseMap.put(key.toLowerCase(Locale.ENGLISH), key); + Object oldValue = super.remove(oldKey); + super.put(key, value); + return oldValue; + } + + @Override + public void putAll(Map m) { + for (Map.Entry entry : m.entrySet()) { + String key = entry.getKey(); + Object value = entry.getValue(); + this.put(key, value); + } + } + + @Override + public Object remove(Object key) { + Object realKey = lowerCaseMap.remove(key.toString().toLowerCase(Locale.ENGLISH)); + return super.remove(realKey); + } + + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/support/FastStringWriter.java b/blade-core-tool/src/main/java/org/springblade/core/tool/support/FastStringWriter.java new file mode 100644 index 0000000..4f15919 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/support/FastStringWriter.java @@ -0,0 +1,103 @@ +package org.springblade.core.tool.support; + +import org.springblade.core.tool.utils.StringPool; +import org.springframework.lang.Nullable; + +import java.io.Writer; + +/** + * FastStringWriter + * + * @author L.cm + */ +public class FastStringWriter extends Writer { + private StringBuilder builder; + + public FastStringWriter() { + builder = new StringBuilder(64); + } + + public FastStringWriter(final int capacity) { + if (capacity < 0) { + throw new IllegalArgumentException("Negative builderfer size"); + } + builder = new StringBuilder(capacity); + } + + public FastStringWriter(@Nullable final StringBuilder builder) { + this.builder = builder != null ? builder : new StringBuilder(64); + } + + /** + * Gets the underlying StringBuilder. + * + * @return StringBuilder + */ + public StringBuilder getBuilder() { + return builder; + } + + @Override + public void write(int c) { + builder.append((char) c); + } + + @Override + public void write(char[] cbuilder, int off, int len) { + if ((off < 0) || (off > cbuilder.length) || (len < 0) || + ((off + len) > cbuilder.length) || ((off + len) < 0)) { + throw new IndexOutOfBoundsException(); + } else if (len == 0) { + return; + } + builder.append(cbuilder, off, len); + } + + @Override + public void write(String str) { + builder.append(str); + } + + @Override + public void write(String str, int off, int len) { + builder.append(str.substring(off, off + len)); + } + + @Override + public FastStringWriter append(CharSequence csq) { + if (csq == null) { + write(StringPool.NULL); + } else { + write(csq.toString()); + } + return this; + } + + @Override + public FastStringWriter append(CharSequence csq, int start, int end) { + CharSequence cs = (csq == null ? StringPool.NULL : csq); + write(cs.subSequence(start, end).toString()); + return this; + } + + @Override + public FastStringWriter append(char c) { + write(c); + return this; + } + + @Override + public String toString() { + return builder.toString(); + } + + @Override + public void flush() { + } + + @Override + public void close() { + builder.setLength(0); + builder.trimToSize(); + } +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/support/IMultiOutputStream.java b/blade-core-tool/src/main/java/org/springblade/core/tool/support/IMultiOutputStream.java new file mode 100644 index 0000000..1afb28a --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/support/IMultiOutputStream.java @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.tool.support; + +import java.io.OutputStream; + +/** + * A factory for creating MultiOutputStream objects. + */ +public interface IMultiOutputStream { + + /** + * Builds the output stream. + * + * @param params the params + * @return the output stream + */ + OutputStream buildOutputStream(Integer... params) ; + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/support/ImagePosition.java b/blade-core-tool/src/main/java/org/springblade/core/tool/support/ImagePosition.java new file mode 100644 index 0000000..de6dca9 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/support/ImagePosition.java @@ -0,0 +1,124 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.tool.support; + +public class ImagePosition { + + /** 图片顶部. */ + public static final int TOP = 32; + + /** 图片中部. */ + public static final int MIDDLE = 16; + + /** 图片底部. */ + public static final int BOTTOM = 8; + + /** 图片左侧. */ + public static final int LEFT = 4; + + /** 图片居中. */ + public static final int CENTER = 2; + + /** 图片右侧. */ + public static final int RIGHT = 1; + + /** 横向边距,靠左或靠右时和边界的距离. */ + private static final int PADDING_HORI = 6; + + /** 纵向边距,靠上或靠底时和边界的距离. */ + private static final int PADDING_VERT = 6; + + + /** 图片中盒[左上角]的x坐标. */ + private int boxPosX ; + + /** 图片中盒[左上角]的y坐标. */ + private int boxPosY ; + + /** + * Instantiates a new image position. + * + * @param width the width + * @param height the height + * @param boxWidth the box width + * @param boxHeight the box height + * @param style the style + */ + public ImagePosition(int width , int height , int boxWidth , int boxHeight, int style ) { + switch(style & 7) { + case LEFT: + boxPosX = PADDING_HORI; + break; + case RIGHT: + boxPosX = width - boxWidth - PADDING_HORI; + break; + case CENTER: + default: + boxPosX = (width - boxWidth)/2; + } + switch(style >> 3 << 3) { + case TOP: + boxPosY = PADDING_VERT; + break; + case MIDDLE: + boxPosY = (height - boxHeight)/2; + break; + case BOTTOM: + default: + boxPosY = height - boxHeight - PADDING_VERT; + } + } + + + /** + * Gets the x. + * + * @return the x + */ + public int getX() { + return getX(0); + } + + /** + * Gets the x. + * + * @param x 横向偏移 + * @return the x + */ + public int getX(int x) { + return this.boxPosX + x; + } + + /** + * Gets the y. + * + * @return the y + */ + public int getY() { + return getY(0); + } + + /** + * Gets the y. + * + * @param y 纵向偏移 + * @return the y + */ + public int getY(int y) { + return this.boxPosY + y; + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/support/StrFormatter.java b/blade-core-tool/src/main/java/org/springblade/core/tool/support/StrFormatter.java new file mode 100644 index 0000000..8c2b729 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/support/StrFormatter.java @@ -0,0 +1,67 @@ +package org.springblade.core.tool.support; + +import org.springblade.core.tool.utils.Func; +import org.springblade.core.tool.utils.StringPool; + +public class StrFormatter { + + /** + * 格式化字符串
+ * 此方法只是简单将占位符 {} 按照顺序替换为参数
+ * 如果想输出 {} 使用 \\转义 { 即可,如果想输出 {} 之前的 \ 使用双转义符 \\\\ 即可
+ * 例:
+ * 通常使用:format("this is {} for {}", "a", "b") =》 this is a for b
+ * 转义{}: format("this is \\{} for {}", "a", "b") =》 this is \{} for a
+ * 转义\: format("this is \\\\{} for {}", "a", "b") =》 this is \a for b
+ * @param strPattern 字符串模板 + * @param argArray 参数列表 + * @return 结果 + */ + public static String format(final String strPattern, final Object... argArray) { + if (Func.isBlank(strPattern) || Func.isEmpty(argArray)) { + return strPattern; + } + final int strPatternLength = strPattern.length(); + + //初始化定义好的长度以获得更好的性能 + StringBuilder sbuf = new StringBuilder(strPatternLength + 50); + + int handledPosition = 0;//记录已经处理到的位置 + int delimIndex;//占位符所在位置 + for (int argIndex = 0; argIndex < argArray.length; argIndex++) { + delimIndex = strPattern.indexOf(StringPool.EMPTY_JSON, handledPosition); + if (delimIndex == -1) {//剩余部分无占位符 + if (handledPosition == 0) { //不带占位符的模板直接返回 + return strPattern; + } else { //字符串模板剩余部分不再包含占位符,加入剩余部分后返回结果 + sbuf.append(strPattern, handledPosition, strPatternLength); + return sbuf.toString(); + } + } else { + if (delimIndex > 0 && strPattern.charAt(delimIndex - 1) == StringPool.BACK_SLASH) {//转义符 + if (delimIndex > 1 && strPattern.charAt(delimIndex - 2) == StringPool.BACK_SLASH) {//双转义符 + //转义符之前还有一个转义符,占位符依旧有效 + sbuf.append(strPattern, handledPosition, delimIndex - 1); + sbuf.append(Func.toStr(argArray[argIndex])); + handledPosition = delimIndex + 2; + } else { + //占位符被转义 + argIndex--; + sbuf.append(strPattern, handledPosition, delimIndex - 1); + sbuf.append(StringPool.LEFT_BRACE); + handledPosition = delimIndex + 1; + } + } else {//正常占位符 + sbuf.append(strPattern, handledPosition, delimIndex); + sbuf.append(Func.toStr(argArray[argIndex])); + handledPosition = delimIndex + 2; + } + } + } + // append the characters following the last {} pair. + //加入最后一个占位符后所有的字符 + sbuf.append(strPattern, handledPosition, strPattern.length()); + + return sbuf.toString(); + } +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/support/StrSpliter.java b/blade-core-tool/src/main/java/org/springblade/core/tool/support/StrSpliter.java new file mode 100644 index 0000000..9c9880a --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/support/StrSpliter.java @@ -0,0 +1,499 @@ +package org.springblade.core.tool.support; + +import org.springblade.core.tool.utils.Func; +import org.springblade.core.tool.utils.StringUtil; +import org.springblade.core.tool.utils.StringPool; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * 字符串切分器 + * @author Looly + * + */ +public class StrSpliter { + + //---------------------------------------------------------------------------------------------- Split by char + /** + * 切分字符串路径,仅支持Unix分界符:/ + * + * @param str 被切分的字符串 + * @return 切分后的集合 + * @since 3.0.8 + */ + public static List splitPath(String str){ + return splitPath(str, 0); + } + + /** + * 切分字符串路径,仅支持Unix分界符:/ + * + * @param str 被切分的字符串 + * @return 切分后的集合 + * @since 3.0.8 + */ + public static String[] splitPathToArray(String str){ + return toArray(splitPath(str)); + } + + /** + * 切分字符串路径,仅支持Unix分界符:/ + * + * @param str 被切分的字符串 + * @param limit 限制分片数 + * @return 切分后的集合 + * @since 3.0.8 + */ + public static List splitPath(String str, int limit){ + return split(str, StringPool.SLASH, limit, true, true); + } + + /** + * 切分字符串路径,仅支持Unix分界符:/ + * + * @param str 被切分的字符串 + * @param limit 限制分片数 + * @return 切分后的集合 + * @since 3.0.8 + */ + public static String[] splitPathToArray(String str, int limit){ + return toArray(splitPath(str, limit)); + } + + /** + * 切分字符串 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符 + * @param ignoreEmpty 是否忽略空串 + * @return 切分后的集合 + * @since 3.2.1 + */ + public static List splitTrim(String str, char separator, boolean ignoreEmpty){ + return split(str, separator, 0, true, ignoreEmpty); + } + + /** + * 切分字符串 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符 + * @param isTrim 是否去除切分字符串后每个元素两边的空格 + * @param ignoreEmpty 是否忽略空串 + * @return 切分后的集合 + * @since 3.0.8 + */ + public static List split(String str, char separator, boolean isTrim, boolean ignoreEmpty){ + return split(str, separator, 0, isTrim, ignoreEmpty); + } + + /** + * 切分字符串,大小写敏感,去除每个元素两边空白符 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符 + * @param limit 限制分片数,-1不限制 + * @param ignoreEmpty 是否忽略空串 + * @return 切分后的集合 + * @since 3.0.8 + */ + public static List splitTrim(String str, char separator, int limit, boolean ignoreEmpty){ + return split(str, separator, limit, true, ignoreEmpty, false); + } + + /** + * 切分字符串,大小写敏感 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符 + * @param limit 限制分片数,-1不限制 + * @param isTrim 是否去除切分字符串后每个元素两边的空格 + * @param ignoreEmpty 是否忽略空串 + * @return 切分后的集合 + * @since 3.0.8 + */ + public static List split(String str, char separator, int limit, boolean isTrim, boolean ignoreEmpty){ + return split(str, separator, limit, isTrim, ignoreEmpty, false); + } + + /** + * 切分字符串,忽略大小写 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符 + * @param limit 限制分片数,-1不限制 + * @param isTrim 是否去除切分字符串后每个元素两边的空格 + * @param ignoreEmpty 是否忽略空串 + * @return 切分后的集合 + * @since 3.2.1 + */ + public static List splitIgnoreCase(String str, char separator, int limit, boolean isTrim, boolean ignoreEmpty){ + return split(str, separator, limit, isTrim, ignoreEmpty, true); + } + + /** + * 切分字符串 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符 + * @param limit 限制分片数,-1不限制 + * @param isTrim 是否去除切分字符串后每个元素两边的空格 + * @param ignoreEmpty 是否忽略空串 + * @param ignoreCase 是否忽略大小写 + * @return 切分后的集合 + * @since 3.2.1 + */ + public static List split(String str, char separator, int limit, boolean isTrim, boolean ignoreEmpty, boolean ignoreCase){ + if(StringUtil.isEmpty(str)){ + return new ArrayList(0); + } + if(limit == 1){ + return addToList(new ArrayList(1), str, isTrim, ignoreEmpty); + } + + final ArrayList list = new ArrayList<>(limit > 0 ? limit : 16); + int len = str.length(); + int start = 0;//切分后每个部分的起始 + for(int i = 0; i < len; i++){ + if(Func.equals(separator, str.charAt(i))){ + addToList(list, str.substring(start, i), isTrim, ignoreEmpty); + start = i+1;//i+1同时将start与i保持一致 + + //检查是否超出范围(最大允许limit-1个,剩下一个留给末尾字符串) + if(limit > 0 && list.size() > limit-2){ + break; + } + } + } + return addToList(list, str.substring(start, len), isTrim, ignoreEmpty);//收尾 + } + + /** + * 切分字符串为字符串数组 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符 + * @param limit 限制分片数 + * @param isTrim 是否去除切分字符串后每个元素两边的空格 + * @param ignoreEmpty 是否忽略空串 + * @return 切分后的集合 + * @since 3.0.8 + */ + public static String[] splitToArray(String str, char separator, int limit, boolean isTrim, boolean ignoreEmpty){ + return toArray(split(str, separator, limit, isTrim, ignoreEmpty)); + } + + //---------------------------------------------------------------------------------------------- Split by String + + /** + * 切分字符串,不忽略大小写 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符串 + * @param isTrim 是否去除切分字符串后每个元素两边的空格 + * @param ignoreEmpty 是否忽略空串 + * @return 切分后的集合 + * @since 3.0.8 + */ + public static List split(String str, String separator, boolean isTrim, boolean ignoreEmpty){ + return split(str, separator, -1, isTrim, ignoreEmpty, false); + } + + /** + * 切分字符串,去除每个元素两边空格,忽略大小写 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符串 + * @param ignoreEmpty 是否忽略空串 + * @return 切分后的集合 + * @since 3.2.1 + */ + public static List splitTrim(String str, String separator, boolean ignoreEmpty){ + return split(str, separator, true, ignoreEmpty); + } + + /** + * 切分字符串,不忽略大小写 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符串 + * @param limit 限制分片数 + * @param isTrim 是否去除切分字符串后每个元素两边的空格 + * @param ignoreEmpty 是否忽略空串 + * @return 切分后的集合 + * @since 3.0.8 + */ + public static List split(String str, String separator, int limit, boolean isTrim, boolean ignoreEmpty){ + return split(str, separator, limit, isTrim, ignoreEmpty, false); + } + + /** + * 切分字符串,去除每个元素两边空格,忽略大小写 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符串 + * @param limit 限制分片数 + * @param ignoreEmpty 是否忽略空串 + * @return 切分后的集合 + * @since 3.2.1 + */ + public static List splitTrim(String str, String separator, int limit, boolean ignoreEmpty){ + return split(str, separator, limit, true, ignoreEmpty); + } + + /** + * 切分字符串,忽略大小写 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符串 + * @param limit 限制分片数 + * @param isTrim 是否去除切分字符串后每个元素两边的空格 + * @param ignoreEmpty 是否忽略空串 + * @return 切分后的集合 + * @since 3.2.1 + */ + public static List splitIgnoreCase(String str, String separator, int limit, boolean isTrim, boolean ignoreEmpty){ + return split(str, separator, limit, isTrim, ignoreEmpty, true); + } + + /** + * 切分字符串,去除每个元素两边空格,忽略大小写 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符串 + * @param limit 限制分片数 + * @param ignoreEmpty 是否忽略空串 + * @return 切分后的集合 + * @since 3.2.1 + */ + public static List splitTrimIgnoreCase(String str, String separator, int limit, boolean ignoreEmpty){ + return split(str, separator, limit, true, ignoreEmpty, true); + } + + /** + * 切分字符串 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符串 + * @param limit 限制分片数 + * @param isTrim 是否去除切分字符串后每个元素两边的空格 + * @param ignoreEmpty 是否忽略空串 + * @param ignoreCase 是否忽略大小写 + * @return 切分后的集合 + * @since 3.2.1 + */ + public static List split(String str, String separator, int limit, boolean isTrim, boolean ignoreEmpty, boolean ignoreCase){ + if(StringUtil.isEmpty(str)){ + return new ArrayList(0); + } + if(limit == 1){ + return addToList(new ArrayList(1), str, isTrim, ignoreEmpty); + } + + if(StringUtil.isEmpty(separator)){//分隔符为空时按照空白符切分 + return split(str, limit); + }else if(separator.length() == 1){//分隔符只有一个字符长度时按照单分隔符切分 + return split(str, separator.charAt(0), limit, isTrim, ignoreEmpty, ignoreCase); + } + + final ArrayList list = new ArrayList<>(); + int len = str.length(); + int separatorLen = separator.length(); + int start = 0; + int i = 0; + while(i < len){ + i = StringUtil.indexOf(str, separator, start, ignoreCase); + if(i > -1){ + addToList(list, str.substring(start, i), isTrim, ignoreEmpty); + start = i + separatorLen; + + //检查是否超出范围(最大允许limit-1个,剩下一个留给末尾字符串) + if(limit > 0 && list.size() > limit-2){ + break; + } + }else{ + break; + } + } + return addToList(list, str.substring(start, len), isTrim, ignoreEmpty); + } + + /** + * 切分字符串为字符串数组 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符 + * @param limit 限制分片数 + * @param isTrim 是否去除切分字符串后每个元素两边的空格 + * @param ignoreEmpty 是否忽略空串 + * @return 切分后的集合 + * @since 3.0.8 + */ + public static String[] splitToArray(String str, String separator, int limit, boolean isTrim, boolean ignoreEmpty){ + return toArray(split(str, separator, limit, isTrim, ignoreEmpty)); + } + + //---------------------------------------------------------------------------------------------- Split by Whitespace + + /** + * 使用空白符切分字符串
+ * 切分后的字符串两边不包含空白符,空串或空白符串并不做为元素之一 + * + * @param str 被切分的字符串 + * @param limit 限制分片数 + * @return 切分后的集合 + * @since 3.0.8 + */ + public static List split(String str, int limit){ + if(StringUtil.isEmpty(str)){ + return new ArrayList(0); + } + if(limit == 1){ + return addToList(new ArrayList(1), str, true, true); + } + + final ArrayList list = new ArrayList<>(); + int len = str.length(); + int start = 0;//切分后每个部分的起始 + for(int i = 0; i < len; i++){ + if(Func.isEmpty(str.charAt(i))){ + addToList(list, str.substring(start, i), true, true); + start = i+1;//i+1同时将start与i保持一致 + + //检查是否超出范围(最大允许limit-1个,剩下一个留给末尾字符串) + if(limit > 0 && list.size() > limit-2){ + break; + } + } + } + return addToList(list, str.substring(start, len), true, true);//收尾 + } + + /** + * 切分字符串为字符串数组 + * + * @param str 被切分的字符串 + * @param limit 限制分片数 + * @return 切分后的集合 + * @since 3.0.8 + */ + public static String[] splitToArray(String str, int limit){ + return toArray(split(str, limit)); + } + + //---------------------------------------------------------------------------------------------- Split by regex + + /** + * 通过正则切分字符串 + * @param str 字符串 + * @param separatorPattern 分隔符正则{@link Pattern} + * @param limit 限制分片数 + * @param isTrim 是否去除切分字符串后每个元素两边的空格 + * @param ignoreEmpty 是否忽略空串 + * @return 切分后的集合 + * @since 3.0.8 + */ + public static List split(String str, Pattern separatorPattern, int limit, boolean isTrim, boolean ignoreEmpty){ + if(StringUtil.isEmpty(str)){ + return new ArrayList(0); + } + if(limit == 1){ + return addToList(new ArrayList(1), str, isTrim, ignoreEmpty); + } + + if(null == separatorPattern){//分隔符为空时按照空白符切分 + return split(str, limit); + } + + final Matcher matcher = separatorPattern.matcher(str); + final ArrayList list = new ArrayList<>(); + int len = str.length(); + int start = 0; + while(matcher.find()){ + addToList(list, str.substring(start, matcher.start()), isTrim, ignoreEmpty); + start = matcher.end(); + + //检查是否超出范围(最大允许limit-1个,剩下一个留给末尾字符串) + if(limit > 0 && list.size() > limit-2){ + break; + } + } + return addToList(list, str.substring(start, len), isTrim, ignoreEmpty); + } + + /** + * 通过正则切分字符串为字符串数组 + * + * @param str 被切分的字符串 + * @param separatorPattern 分隔符正则{@link Pattern} + * @param limit 限制分片数 + * @param isTrim 是否去除切分字符串后每个元素两边的空格 + * @param ignoreEmpty 是否忽略空串 + * @return 切分后的集合 + * @since 3.0.8 + */ + public static String[] splitToArray(String str, Pattern separatorPattern, int limit, boolean isTrim, boolean ignoreEmpty){ + return toArray(split(str, separatorPattern, limit, isTrim, ignoreEmpty)); + } + + //---------------------------------------------------------------------------------------------- Split by length + + /** + * 根据给定长度,将给定字符串截取为多个部分 + * + * @param str 字符串 + * @param len 每一个小节的长度 + * @return 截取后的字符串数组 + */ + public static String[] splitByLength(String str, int len) { + int partCount = str.length() / len; + int lastPartCount = str.length() % len; + int fixPart = 0; + if (lastPartCount != 0) { + fixPart = 1; + } + + final String[] strs = new String[partCount + fixPart]; + for (int i = 0; i < partCount + fixPart; i++) { + if (i == partCount + fixPart - 1 && lastPartCount != 0) { + strs[i] = str.substring(i * len, i * len + lastPartCount); + } else { + strs[i] = str.substring(i * len, i * len + len); + } + } + return strs; + } + + //---------------------------------------------------------------------------------------------------------- Private method start + /** + * 将字符串加入List中 + * @param list 列表 + * @param part 被加入的部分 + * @param isTrim 是否去除两端空白符 + * @param ignoreEmpty 是否略过空字符串(空字符串不做为一个元素) + * @return 列表 + */ + private static List addToList(List list, String part, boolean isTrim, boolean ignoreEmpty){ + part = part.toString(); + if(isTrim){ + part = part.trim(); + } + if(false == ignoreEmpty || false == part.isEmpty()){ + list.add(part); + } + return list; + } + + /** + * List转Array + * @param list List + * @return Array + */ + private static String[] toArray(List list){ + return list.toArray(new String[list.size()]); + } + //---------------------------------------------------------------------------------------------------------- Private method end +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/support/Try.java b/blade-core-tool/src/main/java/org/springblade/core/tool/support/Try.java new file mode 100644 index 0000000..0900248 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/support/Try.java @@ -0,0 +1,29 @@ +package org.springblade.core.tool.support; + +import org.springblade.core.tool.utils.Exceptions; + +import java.util.Objects; +import java.util.function.Function; + +/** + * 当 Lambda 遇上受检异常 + * https://segmentfault.com/a/1190000007832130 + */ +public class Try { + + public static Function of(UncheckedFunction mapper) { + Objects.requireNonNull(mapper); + return t -> { + try { + return mapper.apply(t); + } catch (Exception e) { + throw Exceptions.unchecked(e); + } + }; + } + + @FunctionalInterface + public interface UncheckedFunction { + R apply(T t) throws Exception; + } +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/support/xss/HTMLFilter.java b/blade-core-tool/src/main/java/org/springblade/core/tool/support/xss/HTMLFilter.java new file mode 100644 index 0000000..9d99b42 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/support/xss/HTMLFilter.java @@ -0,0 +1,554 @@ +package org.springblade.core.tool.support.xss; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * HTML filtering utility for protecting against XSS (Cross Site Scripting). + *

+ * This code is licensed LGPLv3 + *

+ * This code is a Java port of the original work in PHP by Cal Hendersen. + * http://code.iamcal.com/php/lib_filter/ + *

+ * The trickiest part of the translation was handling the differences in regex handling + * between PHP and Java. These resources were helpful in the process: + *

+ * http://java.sun.com/j2se/1.4.2/docs/api/java/util/regex/Pattern.html + * http://us2.php.net/manual/en/reference.pcre.pattern.modifiers.php + * http://www.regular-expressions.info/modifiers.html + *

+ * A note on naming conventions: instance variables are prefixed with a "v"; global + * constants are in all caps. + *

+ * Sample use: + * String input = ... + * String clean = new HTMLFilter().filter( input ); + *

+ * The class is not thread safe. Create a new instance if in doubt. + *

+ * If you find bugs or have suggestions on improvement (especially regarding + * performance), please contact us. The latest version of this + * source, and our contact details, can be found at http://xss-html-filter.sf.net + * + * @author Joseph O'Connell + * @author Cal Hendersen + * @author Michael Semb Wever + */ +public final class HTMLFilter { + + /** + * regex flag union representing /si modifiers in php + **/ + private static final int REGEX_FLAGS_SI = Pattern.CASE_INSENSITIVE | Pattern.DOTALL; + private static final Pattern P_COMMENTS = Pattern.compile("", Pattern.DOTALL); + private static final Pattern P_COMMENT = Pattern.compile("^!--(.*)--$", REGEX_FLAGS_SI); + private static final Pattern P_TAGS = Pattern.compile("<(.*?)>", Pattern.DOTALL); + private static final Pattern P_END_TAG = Pattern.compile("^/([a-z0-9]+)", REGEX_FLAGS_SI); + private static final Pattern P_START_TAG = Pattern.compile("^([a-z0-9]+)(.*?)(/?)$", REGEX_FLAGS_SI); + private static final Pattern P_QUOTED_ATTRIBUTES = Pattern.compile("([a-z0-9]+)=([\"'])(.*?)\\2", REGEX_FLAGS_SI); + private static final Pattern P_UNQUOTED_ATTRIBUTES = Pattern.compile("([a-z0-9]+)(=)([^\"\\s']+)", REGEX_FLAGS_SI); + private static final Pattern P_PROTOCOL = Pattern.compile("^([^:]+):", REGEX_FLAGS_SI); + private static final Pattern P_ENTITY = Pattern.compile("&#(\\d+);?"); + private static final Pattern P_ENTITY_UNICODE = Pattern.compile("&#x([0-9a-f]+);?"); + private static final Pattern P_ENCODE = Pattern.compile("%([0-9a-f]{2});?"); + private static final Pattern P_VALID_ENTITIES = Pattern.compile("&([^&;]*)(?=(;|&|$))"); + private static final Pattern P_VALID_QUOTES = Pattern.compile("(>|^)([^<]+?)(<|$)", Pattern.DOTALL); + private static final Pattern P_END_ARROW = Pattern.compile("^>"); + private static final Pattern P_BODY_TO_END = Pattern.compile("<([^>]*?)(?=<|$)"); + private static final Pattern P_XML_CONTENT = Pattern.compile("(^|>)([^<]*?)(?=>)"); + private static final Pattern P_STRAY_LEFT_ARROW = Pattern.compile("<([^>]*?)(?=<|$)"); + private static final Pattern P_STRAY_RIGHT_ARROW = Pattern.compile("(^|>)([^<]*?)(?=>)"); + private static final Pattern P_AMP = Pattern.compile("&"); + private static final Pattern P_QUOTE = Pattern.compile("<"); + private static final Pattern P_LEFT_ARROW = Pattern.compile("<"); + private static final Pattern P_RIGHT_ARROW = Pattern.compile(">"); + private static final Pattern P_BOTH_ARROWS = Pattern.compile("<>"); + + // @xxx could grow large... maybe use sesat's ReferenceMap + private static final ConcurrentMap P_REMOVE_PAIR_BLANKS = new ConcurrentHashMap(); + private static final ConcurrentMap P_REMOVE_SELF_BLANKS = new ConcurrentHashMap(); + + /** + * set of allowed html elements, along with allowed attributes for each element + **/ + private final Map> vAllowed; + /** + * counts of open tags for each (allowable) html element + **/ + private final Map vTagCounts = new HashMap(); + + /** + * html elements which must always be self-closing (e.g. "") + **/ + private final String[] vSelfClosingTags; + /** + * html elements which must always have separate opening and closing tags (e.g. "") + **/ + private final String[] vNeedClosingTags; + /** + * set of disallowed html elements + **/ + private final String[] vDisallowed; + /** + * attributes which should be checked for valid protocols + **/ + private final String[] vProtocolAtts; + /** + * allowed protocols + **/ + private final String[] vAllowedProtocols; + /** + * tags which should be removed if they contain no content (e.g. "" or "") + **/ + private final String[] vRemoveBlanks; + /** + * entities allowed within html markup + **/ + private final String[] vAllowedEntities; + /** + * flag determining whether comments are allowed in input String. + */ + private final boolean stripComment; + private final boolean encodeQuotes; + private boolean vDebug = false; + /** + * flag determining whether to try to make tags when presented with "unbalanced" + * angle brackets (e.g. "" becomes " text "). If set to false, + * unbalanced angle brackets will be html escaped. + */ + private final boolean alwaysMakeTags; + + /** + * Default constructor. + */ + public HTMLFilter() { + vAllowed = new HashMap<>(); + + final ArrayList a_atts = new ArrayList(); + a_atts.add("href"); + a_atts.add("target"); + vAllowed.put("a", a_atts); + + final ArrayList img_atts = new ArrayList(); + img_atts.add("src"); + img_atts.add("width"); + img_atts.add("height"); + img_atts.add("alt"); + vAllowed.put("img", img_atts); + + final ArrayList no_atts = new ArrayList(); + vAllowed.put("b", no_atts); + vAllowed.put("strong", no_atts); + vAllowed.put("i", no_atts); + vAllowed.put("em", no_atts); + + vSelfClosingTags = new String[]{"img"}; + vNeedClosingTags = new String[]{"a", "b", "strong", "i", "em"}; + vDisallowed = new String[]{}; + vAllowedProtocols = new String[]{"http", "mailto", "https"}; // no ftp. + vProtocolAtts = new String[]{"src", "href"}; + vRemoveBlanks = new String[]{"a", "b", "strong", "i", "em"}; + vAllowedEntities = new String[]{"amp", "gt", "lt", "quot"}; + stripComment = true; + encodeQuotes = true; + alwaysMakeTags = true; + } + + /** + * Set debug flag to true. Otherwise use default settings. See the default constructor. + * + * @param debug turn debug on with a true argument + */ + public HTMLFilter(final boolean debug) { + this(); + vDebug = debug; + + } + + /** + * Map-parameter configurable constructor. + * + * @param conf map containing configuration. keys match field names. + */ + public HTMLFilter(final Map conf) { + + assert conf.containsKey("vAllowed") : "configuration requires vAllowed"; + assert conf.containsKey("vSelfClosingTags") : "configuration requires vSelfClosingTags"; + assert conf.containsKey("vNeedClosingTags") : "configuration requires vNeedClosingTags"; + assert conf.containsKey("vDisallowed") : "configuration requires vDisallowed"; + assert conf.containsKey("vAllowedProtocols") : "configuration requires vAllowedProtocols"; + assert conf.containsKey("vProtocolAtts") : "configuration requires vProtocolAtts"; + assert conf.containsKey("vRemoveBlanks") : "configuration requires vRemoveBlanks"; + assert conf.containsKey("vAllowedEntities") : "configuration requires vAllowedEntities"; + + vAllowed = Collections.unmodifiableMap((HashMap>) conf.get("vAllowed")); + vSelfClosingTags = (String[]) conf.get("vSelfClosingTags"); + vNeedClosingTags = (String[]) conf.get("vNeedClosingTags"); + vDisallowed = (String[]) conf.get("vDisallowed"); + vAllowedProtocols = (String[]) conf.get("vAllowedProtocols"); + vProtocolAtts = (String[]) conf.get("vProtocolAtts"); + vRemoveBlanks = (String[]) conf.get("vRemoveBlanks"); + vAllowedEntities = (String[]) conf.get("vAllowedEntities"); + stripComment = conf.containsKey("stripComment") ? (Boolean) conf.get("stripComment") : true; + encodeQuotes = conf.containsKey("encodeQuotes") ? (Boolean) conf.get("encodeQuotes") : true; + alwaysMakeTags = conf.containsKey("alwaysMakeTags") ? (Boolean) conf.get("alwaysMakeTags") : true; + } + + private void reset() { + vTagCounts.clear(); + } + + private void debug(final String msg) { + if (vDebug) { + Logger.getAnonymousLogger().info(msg); + } + } + + //--------------------------------------------------------------- + // my versions of some PHP library functions + public static String chr(final int decimal) { + return String.valueOf((char) decimal); + } + + public static String htmlSpecialChars(final String s) { + String result = s; + result = regexReplace(P_AMP, "&", result); + result = regexReplace(P_QUOTE, """, result); + result = regexReplace(P_LEFT_ARROW, "<", result); + result = regexReplace(P_RIGHT_ARROW, ">", result); + return result; + } + + //--------------------------------------------------------------- + + /** + * given a user submitted input String, filter out any invalid or restricted + * html. + * + * @param input text (i.e. submitted by a user) than may contain html + * @return "clean" version of input, with only valid, whitelisted html elements allowed + */ + public String filter(final String input) { + reset(); + String s = input; + + debug("************************************************"); + debug(" INPUT: " + input); + + s = escapeComments(s); + debug(" escapeComments: " + s); + + s = balanceHTML(s); + debug(" balanceHTML: " + s); + + s = checkTags(s); + debug(" checkTags: " + s); + + s = processRemoveBlanks(s); + debug("processRemoveBlanks: " + s); + + s = validateEntities(s); + debug(" validateEntites: " + s); + + debug("************************************************\n\n"); + return s; + } + + public boolean isAlwaysMakeTags() { + return alwaysMakeTags; + } + + public boolean isStripComments() { + return stripComment; + } + + private String escapeComments(final String s) { + final Matcher m = P_COMMENTS.matcher(s); + final StringBuffer buf = new StringBuffer(); + if (m.find()) { + final String match = m.group(1); //(.*?) + m.appendReplacement(buf, Matcher.quoteReplacement("")); + } + m.appendTail(buf); + + return buf.toString(); + } + + private String balanceHTML(String s) { + if (alwaysMakeTags) { + // + // try and form html + // + s = regexReplace(P_END_ARROW, "", s); + s = regexReplace(P_BODY_TO_END, "<$1>", s); + s = regexReplace(P_XML_CONTENT, "$1<$2", s); + + } else { + // + // escape stray brackets + // + s = regexReplace(P_STRAY_LEFT_ARROW, "<$1", s); + s = regexReplace(P_STRAY_RIGHT_ARROW, "$1$2><", s); + + // + // the last regexp causes '<>' entities to appear + // (we need to do a lookahead assertion so that the last bracket can + // be used in the next pass of the regexp) + // + s = regexReplace(P_BOTH_ARROWS, "", s); + } + + return s; + } + + private String checkTags(String s) { + Matcher m = P_TAGS.matcher(s); + + final StringBuffer buf = new StringBuffer(); + while (m.find()) { + String replaceStr = m.group(1); + replaceStr = processTag(replaceStr); + m.appendReplacement(buf, Matcher.quoteReplacement(replaceStr)); + } + m.appendTail(buf); + + s = buf.toString(); + + // these get tallied in processTag + // (remember to reset before subsequent calls to filter method) + for (String key : vTagCounts.keySet()) { + for (int ii = 0; ii < vTagCounts.get(key); ii++) { + s += ""; + } + } + + return s; + } + + private String processRemoveBlanks(final String s) { + String result = s; + for (String tag : vRemoveBlanks) { + if (!P_REMOVE_PAIR_BLANKS.containsKey(tag)) { + P_REMOVE_PAIR_BLANKS.putIfAbsent(tag, Pattern.compile("<" + tag + "(\\s[^>]*)?>")); + } + result = regexReplace(P_REMOVE_PAIR_BLANKS.get(tag), "", result); + if (!P_REMOVE_SELF_BLANKS.containsKey(tag)) { + P_REMOVE_SELF_BLANKS.putIfAbsent(tag, Pattern.compile("<" + tag + "(\\s[^>]*)?/>")); + } + result = regexReplace(P_REMOVE_SELF_BLANKS.get(tag), "", result); + } + + return result; + } + + private static String regexReplace(final Pattern regex_pattern, final String replacement, final String s) { + Matcher m = regex_pattern.matcher(s); + return m.replaceAll(replacement); + } + + private String processTag(final String s) { + // ending tags + Matcher m = P_END_TAG.matcher(s); + if (m.find()) { + final String name = m.group(1).toLowerCase(); + if (allowed(name)) { + if (!inArray(name, vSelfClosingTags)) { + if (vTagCounts.containsKey(name)) { + vTagCounts.put(name, vTagCounts.get(name) - 1); + return ""; + } + } + } + } + + // starting tags + m = P_START_TAG.matcher(s); + if (m.find()) { + final String name = m.group(1).toLowerCase(); + final String body = m.group(2); + String ending = m.group(3); + + //debug( "in a starting tag, name='" + name + "'; body='" + body + "'; ending='" + ending + "'" ); + if (allowed(name)) { + String params = ""; + + final Matcher m2 = P_QUOTED_ATTRIBUTES.matcher(body); + final Matcher m3 = P_UNQUOTED_ATTRIBUTES.matcher(body); + final List paramNames = new ArrayList(); + final List paramValues = new ArrayList(); + while (m2.find()) { + paramNames.add(m2.group(1)); //([a-z0-9]+) + paramValues.add(m2.group(3)); //(.*?) + } + while (m3.find()) { + paramNames.add(m3.group(1)); //([a-z0-9]+) + paramValues.add(m3.group(3)); //([^\"\\s']+) + } + + String paramName, paramValue; + for (int ii = 0; ii < paramNames.size(); ii++) { + paramName = paramNames.get(ii).toLowerCase(); + paramValue = paramValues.get(ii); + +// debug( "paramName='" + paramName + "'" ); +// debug( "paramValue='" + paramValue + "'" ); +// debug( "allowed? " + vAllowed.get( name ).contains( paramName ) ); + + if (allowedAttribute(name, paramName)) { + if (inArray(paramName, vProtocolAtts)) { + paramValue = processParamProtocol(paramValue); + } + params += " " + paramName + "=\"" + paramValue + "\""; + } + } + + if (inArray(name, vSelfClosingTags)) { + ending = " /"; + } + + if (inArray(name, vNeedClosingTags)) { + ending = ""; + } + + if (ending == null || ending.length() < 1) { + if (vTagCounts.containsKey(name)) { + vTagCounts.put(name, vTagCounts.get(name) + 1); + } else { + vTagCounts.put(name, 1); + } + } else { + ending = " /"; + } + return "<" + name + params + ending + ">"; + } else { + return ""; + } + } + + // comments + m = P_COMMENT.matcher(s); + if (!stripComment && m.find()) { + return "<" + m.group() + ">"; + } + + return ""; + } + + private String processParamProtocol(String s) { + s = decodeEntities(s); + final Matcher m = P_PROTOCOL.matcher(s); + if (m.find()) { + final String protocol = m.group(1); + if (!inArray(protocol, vAllowedProtocols)) { + // bad protocol, turn into local anchor link instead + s = "#" + s.substring(protocol.length() + 1, s.length()); + if (s.startsWith("#//")) { + s = "#" + s.substring(3, s.length()); + } + } + } + + return s; + } + + private String decodeEntities(String s) { + StringBuffer buf = new StringBuffer(); + + Matcher m = P_ENTITY.matcher(s); + while (m.find()) { + final String match = m.group(1); + final int decimal = Integer.decode(match).intValue(); + m.appendReplacement(buf, Matcher.quoteReplacement(chr(decimal))); + } + m.appendTail(buf); + s = buf.toString(); + + buf = new StringBuffer(); + m = P_ENTITY_UNICODE.matcher(s); + while (m.find()) { + final String match = m.group(1); + final int decimal = Integer.valueOf(match, 16).intValue(); + m.appendReplacement(buf, Matcher.quoteReplacement(chr(decimal))); + } + m.appendTail(buf); + s = buf.toString(); + + buf = new StringBuffer(); + m = P_ENCODE.matcher(s); + while (m.find()) { + final String match = m.group(1); + final int decimal = Integer.valueOf(match, 16).intValue(); + m.appendReplacement(buf, Matcher.quoteReplacement(chr(decimal))); + } + m.appendTail(buf); + s = buf.toString(); + + s = validateEntities(s); + return s; + } + + private String validateEntities(final String s) { + StringBuffer buf = new StringBuffer(); + + // validate entities throughout the string + Matcher m = P_VALID_ENTITIES.matcher(s); + while (m.find()) { + final String one = m.group(1); //([^&;]*) + final String two = m.group(2); //(?=(;|&|$)) + m.appendReplacement(buf, Matcher.quoteReplacement(checkEntity(one, two))); + } + m.appendTail(buf); + + return encodeQuotes(buf.toString()); + } + + private String encodeQuotes(final String s) { + if (encodeQuotes) { + StringBuffer buf = new StringBuffer(); + Matcher m = P_VALID_QUOTES.matcher(s); + while (m.find()) { + final String one = m.group(1); //(>|^) + final String two = m.group(2); //([^<]+?) + final String three = m.group(3); //(<|$) + m.appendReplacement(buf, Matcher.quoteReplacement(one + regexReplace(P_QUOTE, """, two) + three)); + } + m.appendTail(buf); + return buf.toString(); + } else { + return s; + } + } + + private String checkEntity(final String preamble, final String term) { + + return ";".equals(term) && isValidEntity(preamble) + ? '&' + preamble + : "&" + preamble; + } + + private boolean isValidEntity(final String entity) { + return inArray(entity, vAllowedEntities); + } + + private static boolean inArray(final String s, final String[] array) { + for (String item : array) { + if (item != null && item.equals(s)) { + return true; + } + } + return false; + } + + private boolean allowed(final String name) { + return (vAllowed.isEmpty() || vAllowed.containsKey(name)) && !inArray(name, vDisallowed); + } + + private boolean allowedAttribute(final String name, final String paramName) { + return allowed(name) && (vAllowed.isEmpty() || vAllowed.get(name).contains(paramName)); + } +} \ No newline at end of file diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/support/xss/SQLFilter.java b/blade-core-tool/src/main/java/org/springblade/core/tool/support/xss/SQLFilter.java new file mode 100644 index 0000000..0e0a1f8 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/support/xss/SQLFilter.java @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.tool.support.xss; + + +import org.springblade.core.tool.utils.StringUtil; + +/** + * SQL过滤 + */ +public class SQLFilter { + + /** + * SQL注入过滤 + * + * @param str 待验证的字符串 + */ + public static String sqlInject(String str) { + if (StringUtil.isBlank(str)) { + return null; + } + //去掉'|"|;|\字符 + str = str.replace("'", ""); + str = str.replace("\"", ""); + str = str.replace(";", ""); + str = str.replace("\\", ""); + + //转换成小写 + str = str.toLowerCase(); + + //非法字符 + String[] keywords = {"master", "truncate", "insert", "select", "delete", "update", "declare", "alert", "drop"}; + + //判断是否包含非法字符 + for (String keyword : keywords) { + if (str.indexOf(keyword) != -1) { + throw new RuntimeException("包含非法字符"); + } + } + + return str; + } +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/support/xss/XssFilter.java b/blade-core-tool/src/main/java/org/springblade/core/tool/support/xss/XssFilter.java new file mode 100644 index 0000000..bb86a80 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/support/xss/XssFilter.java @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.tool.support.xss; + +import javax.servlet.*; +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; + +/** + * XSS过滤 + */ +public class XssFilter implements Filter { + + @Override + public void init(FilterConfig config) throws ServletException { + + } + + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + ServletRequest requestWrapper = null; + if(request instanceof HttpServletRequest) { + requestWrapper = new XssHttpServletRequestWrapper((HttpServletRequest) request); + } + if(requestWrapper == null) { + chain.doFilter(request, response); + } else { + chain.doFilter(requestWrapper, response); + } + } + + @Override + public void destroy() { + + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/support/xss/XssHttpServletRequestWrapper.java b/blade-core-tool/src/main/java/org/springblade/core/tool/support/xss/XssHttpServletRequestWrapper.java new file mode 100644 index 0000000..184305d --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/support/xss/XssHttpServletRequestWrapper.java @@ -0,0 +1,174 @@ +/** + * Copyright (c) 2018-2028, Chill Zhuang 庄骞 (smallchill@163.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.tool.support.xss; + +import org.springblade.core.tool.utils.Charsets; +import org.springblade.core.tool.utils.StringUtil; +import org.springblade.core.tool.utils.WebUtil; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; + +import javax.servlet.ReadListener; +import javax.servlet.ServletInputStream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * XSS过滤处理 + */ +public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper { + + //没被包装过的HttpServletRequest(特殊场景,需要自己过滤) + HttpServletRequest orgRequest; + //html过滤 + private final static HTMLFilter htmlFilter = new HTMLFilter(); + // 缓存报文,支持多次读取流 + private final byte[] body; + + public XssHttpServletRequestWrapper(HttpServletRequest request) throws IOException { + super(request); + orgRequest = request; + body = WebUtil.getRequestBytes(request); + } + + @Override + public BufferedReader getReader() throws IOException { + return new BufferedReader(new InputStreamReader(getInputStream())); + } + + @Override + public ServletInputStream getInputStream() throws IOException { + + //为空,直接返回 + if (null == super.getHeader(HttpHeaders.CONTENT_TYPE)) { + return super.getInputStream(); + } + + //非json类型,直接返回 + if (!super.getHeader(HttpHeaders.CONTENT_TYPE).equalsIgnoreCase(MediaType.APPLICATION_JSON_VALUE) + && !super.getHeader(HttpHeaders.CONTENT_TYPE).equalsIgnoreCase(MediaType.APPLICATION_JSON_UTF8_VALUE)) { + return super.getInputStream(); + } + + //为空,直接返回 + String requestStr = WebUtil.getRequestStr(orgRequest, body); + if (StringUtil.isBlank(requestStr)) { + return super.getInputStream(); + } + + requestStr = xssEncode(requestStr); + + final ByteArrayInputStream bis = new ByteArrayInputStream(requestStr.getBytes(Charsets.UTF_8)); + + return new ServletInputStream() { + + @Override + public boolean isFinished() { + return true; + } + + @Override + public boolean isReady() { + return true; + } + + @Override + public void setReadListener(ReadListener readListener) { + + } + + @Override + public int read() throws IOException { + return bis.read(); + } + }; + + } + + @Override + public String getParameter(String name) { + String value = super.getParameter(xssEncode(name)); + if (StringUtil.isNotBlank(value)) { + value = xssEncode(value); + } + return value; + } + + @Override + public String[] getParameterValues(String name) { + String[] parameters = super.getParameterValues(name); + if (parameters == null || parameters.length == 0) { + return null; + } + + for (int i = 0; i < parameters.length; i++) { + parameters[i] = xssEncode(parameters[i]); + } + return parameters; + } + + @Override + public Map getParameterMap() { + Map map = new LinkedHashMap<>(); + Map parameters = super.getParameterMap(); + for (String key : parameters.keySet()) { + String[] values = parameters.get(key); + for (int i = 0; i < values.length; i++) { + values[i] = xssEncode(values[i]); + } + map.put(key, values); + } + return map; + } + + @Override + public String getHeader(String name) { + String value = super.getHeader(xssEncode(name)); + if (StringUtil.isNotBlank(value)) { + value = xssEncode(value); + } + return value; + } + + private String xssEncode(String input) { + return htmlFilter.filter(input); + } + + /** + * 获取最原始的request + */ + public HttpServletRequest getOrgRequest() { + return orgRequest; + } + + /** + * 获取最原始的request + */ + public static HttpServletRequest getOrgRequest(HttpServletRequest request) { + if (request instanceof XssHttpServletRequestWrapper) { + return ((XssHttpServletRequestWrapper) request).getOrgRequest(); + } + + return request; + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/utils/AESUtil.java b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/AESUtil.java new file mode 100644 index 0000000..93168c2 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/AESUtil.java @@ -0,0 +1,126 @@ +/** + * Copyright (c) 2018-2028, DreamLu 卢春梦 (qq596392912@gmail.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.tool.utils; + +import org.springframework.util.Assert; + +import javax.crypto.Cipher; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import java.util.Arrays; + +/** + * 完全兼容微信所使用的AES加密方式。 + * aes的key必须是256byte长(比如32个字符),可以使用AesKit.genAesKey()来生成一组key + * + * @author L.cm + */ +public class AESUtil { + + private AESUtil() {} + + public static String genAesKey() { + return StringUtil.random(32); + } + + public static byte[] encrypt(byte[] content, String aesTextKey) { + return encrypt(content, aesTextKey.getBytes(Charsets.UTF_8)); + } + + public static byte[] encrypt(String content, String aesTextKey) { + return encrypt(content.getBytes(Charsets.UTF_8), aesTextKey.getBytes(Charsets.UTF_8)); + } + + public static byte[] encrypt(String content, java.nio.charset.Charset charset, String aesTextKey) { + return encrypt(content.getBytes(charset), aesTextKey.getBytes(Charsets.UTF_8)); + } + + public static byte[] decrypt(byte[] content, String aesTextKey) { + return decrypt(content, aesTextKey.getBytes(Charsets.UTF_8)); + } + + public static String decryptToStr(byte[] content, String aesTextKey) { + return new String(decrypt(content, aesTextKey.getBytes(Charsets.UTF_8)), Charsets.UTF_8); + } + + public static String decryptToStr(byte[] content, String aesTextKey, java.nio.charset.Charset charset) { + return new String(decrypt(content, aesTextKey.getBytes(Charsets.UTF_8)), charset); + } + + public static byte[] encrypt(byte[] content, byte[] aesKey) { + Assert.isTrue(aesKey.length == 32, "IllegalAesKey, aesKey's length must be 32"); + try { + Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding"); + SecretKeySpec keySpec = new SecretKeySpec(aesKey, "AES"); + IvParameterSpec iv = new IvParameterSpec(aesKey, 0, 16); + cipher.init(Cipher.ENCRYPT_MODE, keySpec, iv); + return cipher.doFinal(PKCS7Encoder.encode(content)); + } catch (Exception e) { + throw Exceptions.unchecked(e); + } + } + + public static byte[] decrypt(byte[] encrypted, byte[] aesKey) { + Assert.isTrue(aesKey.length == 32, "IllegalAesKey, aesKey's length must be 32"); + try { + Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding"); + SecretKeySpec keySpec = new SecretKeySpec(aesKey, "AES"); + IvParameterSpec iv = new IvParameterSpec(Arrays.copyOfRange(aesKey, 0, 16)); + cipher.init(Cipher.DECRYPT_MODE, keySpec, iv); + return PKCS7Encoder.decode(cipher.doFinal(encrypted)); + } catch (Exception e) { + throw Exceptions.unchecked(e); + } + } + + /** + * 提供基于PKCS7算法的加解密接口. + */ + static class PKCS7Encoder { + static int BLOCK_SIZE = 32; + + static byte[] encode(byte[] src) { + int count = src.length; + // 计算需要填充的位数 + int amountToPad = BLOCK_SIZE - (count % BLOCK_SIZE); + if (amountToPad == 0) { + amountToPad = BLOCK_SIZE; + } + // 获得补位所用的字符 + byte pad = (byte) (amountToPad & 0xFF); + byte[] pads = new byte[amountToPad]; + for (int index = 0; index < amountToPad; index++) { + pads[index] = pad; + } + int length = count + amountToPad; + byte[] dest = new byte[length]; + System.arraycopy(src, 0, dest, 0, count); + System.arraycopy(pads, 0, dest, count, amountToPad); + return dest; + } + + static byte[] decode(byte[] decrypted) { + int pad = (int) decrypted[decrypted.length - 1]; + if (pad < 1 || pad > 32) { + pad = 0; + } + if (pad > 0) { + return Arrays.copyOfRange(decrypted, 0, decrypted.length - pad); + } + return decrypted; + } + } +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/utils/Base64Util.java b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/Base64Util.java new file mode 100644 index 0000000..eb7b0aa --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/Base64Util.java @@ -0,0 +1,114 @@ +/** + * Copyright (c) 2018-2028, DreamLu 卢春梦 (qq596392912@gmail.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.tool.utils; + +/** + * Base64工具 + * + * @author L.cm + */ +public class Base64Util extends org.springframework.util.Base64Utils { + + /** + * 编码 + * + * @param value 字符串 + * @return {String} + */ + public static String encode(String value) { + return Base64Util.encode(value, Charsets.UTF_8); + } + + /** + * 编码 + * + * @param value 字符串 + * @param charset 字符集 + * @return {String} + */ + public static String encode(String value, java.nio.charset.Charset charset) { + byte[] val = value.getBytes(charset); + return new String(Base64Util.encode(val), charset); + } + + /** + * 编码URL安全 + * + * @param value 字符串 + * @return {String} + */ + public static String encodeUrlSafe(String value) { + return Base64Util.encodeUrlSafe(value, Charsets.UTF_8); + } + + /** + * 编码URL安全 + * + * @param value 字符串 + * @param charset 字符集 + * @return {String} + */ + public static String encodeUrlSafe(String value, java.nio.charset.Charset charset) { + byte[] val = value.getBytes(charset); + return new String(Base64Util.encodeUrlSafe(val), charset); + } + + /** + * 解码 + * + * @param value 字符串 + * @return {String} + */ + public static String decode(String value) { + return Base64Util.decode(value, Charsets.UTF_8); + } + + /** + * 解码 + * + * @param value 字符串 + * @param charset 字符集 + * @return {String} + */ + public static String decode(String value, java.nio.charset.Charset charset) { + byte[] val = value.getBytes(charset); + byte[] decodedValue = Base64Util.decode(val); + return new String(decodedValue, charset); + } + + /** + * 解码URL安全 + * + * @param value 字符串 + * @return {String} + */ + public static String decodeUrlSafe(String value) { + return Base64Util.decodeUrlSafe(value, Charsets.UTF_8); + } + + /** + * 解码URL安全 + * + * @param value 字符串 + * @param charset 字符集 + * @return {String} + */ + public static String decodeUrlSafe(String value, java.nio.charset.Charset charset) { + byte[] val = value.getBytes(charset); + byte[] decodedValue = Base64Util.decodeUrlSafe(val); + return new String(decodedValue, charset); + } +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/utils/BeanUtil.java b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/BeanUtil.java new file mode 100644 index 0000000..2912f6c --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/BeanUtil.java @@ -0,0 +1,200 @@ +/** + * Copyright (c) 2018-2028, DreamLu 卢春梦 (qq596392912@gmail.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.tool.utils; + + +import org.springblade.core.tool.support.BeanProperty; +import org.springframework.beans.BeansException; +import org.springframework.cglib.beans.BeanCopier; +import org.springframework.cglib.beans.BeanGenerator; +import org.springframework.cglib.beans.BeanMap; +import org.springframework.util.Assert; + +import java.util.Map; + +/** + * 实体工具类 + * + * @author L.cm + */ +public class BeanUtil extends org.springframework.beans.BeanUtils { + + /** + * 实例化对象 + * @param clazz 类 + * @param 泛型标记 + * @return 对象 + */ + @SuppressWarnings("unchecked") + public static T newInstance(Class clazz) { + return (T) instantiateClass(clazz); + } + + /** + * 实例化对象 + * @param clazzStr 类名 + * @param 泛型标记 + * @return 对象 + */ + public static T newInstance(String clazzStr) { + try { + Class clazz = Class.forName(clazzStr); + return newInstance(clazz); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + + /** + * 获取Bean的属性 + * @param bean bean + * @param propertyName 属性名 + * @return 属性值 + */ + public static Object getProperty(Object bean, String propertyName) { + Assert.notNull(bean, "bean Could not null"); + return BeanMap.create(bean).get(propertyName); + } + + /** + * 设置Bean属性 + * @param bean bean + * @param propertyName 属性名 + * @param value 属性值 + */ + public static void setProperty(Object bean, String propertyName, Object value) { + Assert.notNull(bean, "bean Could not null"); + BeanMap.create(bean).put(propertyName, value); + } + + /** + * 深复制 + * + * 注意:不支持链式Bean + * + * @param source 源对象 + * @param 泛型标记 + * @return T + */ + @SuppressWarnings("unchecked") + public static T clone(T source) { + return (T) BeanUtil.copy(source, source.getClass()); + } + + /** + * copy 对象属性到另一个对象,默认不使用Convert + * + * 注意:不支持链式Bean,链式用 copyProperties + * + * @param source 源对象 + * @param clazz 类名 + * @param 泛型标记 + * @return T + */ + public static T copy(Object source, Class clazz) { + BeanCopier copier = BeanCopier.create(source.getClass(), clazz, false); + + T to = newInstance(clazz); + copier.copy(source, to, null); + return to; + } + + /** + * 拷贝对象 + * + * 注意:不支持链式Bean,链式用 copyProperties + * + * @param source 源对象 + * @param targetBean 需要赋值的对象 + */ + public static void copy(Object source, Object targetBean) { + BeanCopier copier = BeanCopier + .create(source.getClass(), targetBean.getClass(), false); + + copier.copy(source, targetBean, null); + } + + /** + * Copy the property values of the given source bean into the target class. + *

Note: The source and target classes do not have to match or even be derived + * from each other, as long as the properties match. Any bean properties that the + * source bean exposes but the target bean does not will silently be ignored. + *

This is just a convenience method. For more complex transfer needs, + * @param source the source bean + * @param target the target bean class + * @param 泛型标记 + * @throws BeansException if the copying failed + * @return T + */ + public static T copyProperties(Object source, Class target) throws BeansException { + T to = newInstance(target); + BeanUtil.copyProperties(source, to); + return to; + } + + /** + * 将对象装成map形式 + * @param bean 源对象 + * @return {Map} + */ + @SuppressWarnings("unchecked") + public static Map toMap(Object bean) { + return BeanMap.create(bean); + } + + /** + * 将map 转为 bean + * @param beanMap map + * @param valueType 对象类型 + * @param 泛型标记 + * @return {T} + */ + public static T toBean(Map beanMap, Class valueType) { + T bean = BeanUtil.newInstance(valueType); + BeanMap.create(bean).putAll(beanMap); + return bean; + } + + /** + * 给一个Bean添加字段 + * @param superBean 父级Bean + * @param props 新增属性 + * @return {Object} + */ + public static Object generator(Object superBean, BeanProperty... props) { + Class superclass = superBean.getClass(); + Object genBean = generator(superclass, props); + BeanUtil.copy(superBean, genBean); + return genBean; + } + + /** + * 给一个class添加字段 + * @param superclass 父级 + * @param props 新增属性 + * @return {Object} + */ + public static Object generator(Class superclass, BeanProperty... props) { + BeanGenerator generator = new BeanGenerator(); + generator.setSuperclass(superclass); + generator.setUseCache(true); + for (BeanProperty prop : props) { + generator.addProperty(prop.getName(), prop.getType()); + } + return generator.create(); + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/utils/Charsets.java b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/Charsets.java new file mode 100644 index 0000000..16603d2 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/Charsets.java @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2018-2028, DreamLu 卢春梦 (qq596392912@gmail.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.tool.utils; + + +import java.nio.charset.StandardCharsets; +import java.nio.charset.UnsupportedCharsetException; + +/** + * 字符集工具类 + */ +public class Charsets { + + /** + * 字符集ISO-8859-1 + */ + public static final java.nio.charset.Charset ISO_8859_1 = StandardCharsets.ISO_8859_1; + /** + * 字符集GBK + */ + public static final java.nio.charset.Charset GBK = java.nio.charset.Charset.forName(StringPool.GBK); + /** + * 字符集utf-8 + */ + public static final java.nio.charset.Charset UTF_8 = StandardCharsets.UTF_8; + + /** + * 转换为Charset对象 + * @param charsetName 字符集,为空则返回默认字符集 + * @return Charsets + * @throws UnsupportedCharsetException 编码不支持 + */ + public static java.nio.charset.Charset charset(String charsetName) throws UnsupportedCharsetException { + return StringUtil.isBlank(charsetName) ? java.nio.charset.Charset.defaultCharset() : java.nio.charset.Charset.forName(charsetName); + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/utils/ClassUtil.java b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/ClassUtil.java new file mode 100644 index 0000000..997af1f --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/ClassUtil.java @@ -0,0 +1,104 @@ +/** + * Copyright (c) 2018-2028, DreamLu 卢春梦 (qq596392912@gmail.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.tool.utils; + +import org.springframework.core.BridgeMethodResolver; +import org.springframework.core.DefaultParameterNameDiscoverer; +import org.springframework.core.MethodParameter; +import org.springframework.core.ParameterNameDiscoverer; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.core.annotation.SynthesizingMethodParameter; +import org.springframework.web.method.HandlerMethod; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; + +/** + * 类工具类 + * + * @author L.cm + */ +public class ClassUtil extends org.springframework.util.ClassUtils { + + private static final ParameterNameDiscoverer PARAMETER_NAME_DISCOVERER = new DefaultParameterNameDiscoverer(); + + /** + * 获取方法参数信息 + * @param constructor 构造器 + * @param parameterIndex 参数序号 + * @return {MethodParameter} + */ + public static MethodParameter getMethodParameter(Constructor constructor, int parameterIndex) { + MethodParameter methodParameter = new SynthesizingMethodParameter(constructor, parameterIndex); + methodParameter.initParameterNameDiscovery(PARAMETER_NAME_DISCOVERER); + return methodParameter; + } + + /** + * 获取方法参数信息 + * @param method 方法 + * @param parameterIndex 参数序号 + * @return {MethodParameter} + */ + public static MethodParameter getMethodParameter(Method method, int parameterIndex) { + MethodParameter methodParameter = new SynthesizingMethodParameter(method, parameterIndex); + methodParameter.initParameterNameDiscovery(PARAMETER_NAME_DISCOVERER); + return methodParameter; + } + + /** + * 获取Annotation + * @param method Method + * @param annotationType 注解类 + * @param 泛型标记 + * @return {Annotation} + */ + public static A getAnnotation(Method method, Class annotationType) { + Class targetClass = method.getDeclaringClass(); + // The method may be on an interface, but we need attributes from the target class. + // If the target class is null, the method will be unchanged. + Method specificMethod = ClassUtil.getMostSpecificMethod(method, targetClass); + // If we are dealing with method with generic parameters, find the original method. + specificMethod = BridgeMethodResolver.findBridgedMethod(specificMethod); + // 先找方法,再找方法上的类 + A annotation = AnnotatedElementUtils.findMergedAnnotation(specificMethod, annotationType);; + if (null != annotation) { + return annotation; + } + // 获取类上面的Annotation,可能包含组合注解,故采用spring的工具类 + return AnnotatedElementUtils.findMergedAnnotation(specificMethod.getDeclaringClass(), annotationType); + } + + /** + * 获取Annotation + * @param handlerMethod HandlerMethod + * @param annotationType 注解类 + * @param 泛型标记 + * @return {Annotation} + */ + public static A getAnnotation(HandlerMethod handlerMethod, Class annotationType) { + // 先找方法,再找方法上的类 + A annotation = handlerMethod.getMethodAnnotation(annotationType); + if (null != annotation) { + return annotation; + } + // 获取类上面的Annotation,可能包含组合注解,故采用spring的工具类 + Class beanType = handlerMethod.getBeanType(); + return AnnotatedElementUtils.findMergedAnnotation(beanType, annotationType); + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/utils/CollectionUtil.java b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/CollectionUtil.java new file mode 100644 index 0000000..0d1dc22 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/CollectionUtil.java @@ -0,0 +1,75 @@ +/** + * Copyright (c) 2018-2028, DreamLu 卢春梦 (qq596392912@gmail.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.tool.utils; + +import org.springframework.lang.Nullable; +import org.springframework.util.CollectionUtils; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Map; + +/** + * 集合工具类 + * + * @author L.cm + */ +public class CollectionUtil extends org.springframework.util.CollectionUtils { + + /** + * Check whether the given Array contains the given element. + * @param array the Array to check + * @param element the element to look for + * @param The generic tag + * @return {@code true} if found, {@code false} else + */ + public static boolean contains(@Nullable T[] array, final T element) { + if (array == null) { return false; } + return Arrays.stream(array).anyMatch(x -> ObjectUtil.nullSafeEquals(x, element)); + } + + /** + * 对象是否为数组对象 + * + * @param obj 对象 + * @return 是否为数组对象,如果为{@code null} 返回false + */ + public static boolean isArray(Object obj) { + if (null == obj) { + return false; + } + return obj.getClass().isArray(); + } + + /** + * Determine whether the given Collection is not empty: + * i.e. {@code null} or of zero length. + * @param coll the Collection to check + */ + public static boolean isNotEmpty(@Nullable Collection coll) { + return !CollectionUtils.isEmpty(coll); + } + + /** + * Determine whether the given Map is not empty: + * i.e. {@code null} or of zero length. + * @param map the Map to check + */ + public static boolean isNotEmpty(@Nullable Map map) { + return !CollectionUtils.isEmpty(map); + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/utils/DateTimeUtil.java b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/DateTimeUtil.java new file mode 100644 index 0000000..83961df --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/DateTimeUtil.java @@ -0,0 +1,117 @@ +/** + * Copyright (c) 2018-2028, DreamLu 卢春梦 (qq596392912@gmail.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.tool.utils; + +import org.springblade.core.tool.date.DatePattern; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.time.temporal.TemporalAccessor; + +/** + * DateTime 工具类 + * + * @author L.cm + */ +public class DateTimeUtil { + public static final DateTimeFormatter DATETIME_FORMAT = DateTimeFormatter.ofPattern(DatePattern.NORM_DATETIME_PATTERN); + public static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern(DatePattern.NORM_DATE_PATTERN); + public static final DateTimeFormatter TIME_FORMAT = DateTimeFormatter.ofPattern(DatePattern.NORM_TIME_PATTERN); + + /** + * 日期时间格式化 + * + * @param temporal 时间 + * @return 格式化后的时间 + */ + public static String formatDateTime(TemporalAccessor temporal) { + return DATETIME_FORMAT.format(temporal); + } + + /** + * 日期时间格式化 + * + * @param temporal 时间 + * @return 格式化后的时间 + */ + public static String formatDate(TemporalAccessor temporal) { + return DATE_FORMAT.format(temporal); + } + + /** + * 时间格式化 + * + * @param temporal 时间 + * @return 格式化后的时间 + */ + public static String formatTime(TemporalAccessor temporal) { + return TIME_FORMAT.format(temporal); + } + + /** + * 日期格式化 + * + * @param temporal 时间 + * @param pattern 表达式 + * @return 格式化后的时间 + */ + public static String format(TemporalAccessor temporal, String pattern) { + return DateTimeFormatter.ofPattern(pattern).format(temporal); + } + + /** + * 将字符串转换为时间 + * + * @param dateStr 时间字符串 + * @param pattern 表达式 + * @return 时间 + */ + public static TemporalAccessor parse(String dateStr, String pattern) { + DateTimeFormatter format = DateTimeFormatter.ofPattern(pattern); + return format.parse(dateStr); + } + + /** + * 将字符串转换为时间 + * + * @param dateStr 时间字符串 + * @param formatter DateTimeFormatter + * @return 时间 + */ + public static TemporalAccessor parse(String dateStr, DateTimeFormatter formatter) { + return formatter.parse(dateStr); + } + + /** + * 时间转 Instant + * @param dateTime 时间 + * @return Instant + */ + public static Instant toInstant(LocalDateTime dateTime) { + return dateTime.atZone(ZoneId.systemDefault()).toInstant(); + } + + /** + * Instant 转 时间 + * @param instant Instant + * @return Instant + */ + public static LocalDateTime toDateTime(Instant instant) { + return LocalDateTime.ofInstant(instant, ZoneId.systemDefault()); + } +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/utils/DigestUtil.java b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/DigestUtil.java new file mode 100644 index 0000000..38e7110 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/DigestUtil.java @@ -0,0 +1,116 @@ +/** + * Copyright (c) 2018-2028, DreamLu 卢春梦 (qq596392912@gmail.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.tool.utils; + +import org.springframework.lang.Nullable; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +/** + * 加密相关工具类直接使用Spring util封装,减少jar依赖 + * + * @author L.cm + */ +public class DigestUtil extends org.springframework.util.DigestUtils { + + /** + * Calculates the MD5 digest and returns the value as a 32 character hex string. + * + * @param data Data to digest + * @return MD5 digest as a hex string + */ + public static String md5Hex(final String data) { + return DigestUtil.md5DigestAsHex(data.getBytes(Charsets.UTF_8)); + } + + /** + * Return a hexadecimal string representation of the MD5 digest of the given bytes. + * @param bytes the bytes to calculate the digest over + * @return a hexadecimal digest string + */ + public static String md5Hex(final byte[] bytes) { + return DigestUtil.md5DigestAsHex(bytes); + } + + private static final char[] HEX_DIGITS = "0123456789abcdef".toCharArray(); + + public static String sha1(String srcStr){ + return hash("SHA-1", srcStr); + } + + public static String sha256(String srcStr){ + return hash("SHA-256", srcStr); + } + + public static String sha384(String srcStr){ + return hash("SHA-384", srcStr); + } + + public static String sha512(String srcStr){ + return hash("SHA-512", srcStr); + } + + public static String hash(String algorithm, String srcStr) { + try { + MessageDigest md = MessageDigest.getInstance(algorithm); + byte[] bytes = md.digest(srcStr.getBytes(Charsets.UTF_8)); + return toHex(bytes); + } catch (NoSuchAlgorithmException e) { + throw Exceptions.unchecked(e); + } + } + + public static String toHex(byte[] bytes) { + StringBuilder ret = new StringBuilder(bytes.length * 2); + for (int i=0; i> 4) & 0x0f]); + ret.append(HEX_DIGITS[bytes[i] & 0x0f]); + } + return ret.toString(); + } + + public static boolean slowEquals(@Nullable String a, @Nullable String b) { + if (a == null || b == null) { + return false; + } + return slowEquals(a.getBytes(Charsets.UTF_8), b.getBytes(Charsets.UTF_8)); + } + + public static boolean slowEquals(@Nullable byte[] a, @Nullable byte[] b) { + if (a == null || b == null) { + return false; + } + if (a.length != b.length) { + return false; + } + int diff = a.length ^ b.length; + for(int i=0; i + * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.tool.utils; + +import org.springblade.core.tool.support.FastStringWriter; + +import java.io.PrintWriter; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.UndeclaredThrowableException; + +/** + * 异常处理工具类 + */ +public class Exceptions { + + /** + * 将CheckedException转换为UncheckedException. + * + * @param e Throwable + * @return {RuntimeException} + */ + public static RuntimeException unchecked(Throwable e) { + if (e instanceof IllegalAccessException || e instanceof IllegalArgumentException + || e instanceof NoSuchMethodException) { + return new IllegalArgumentException(e); + } else if (e instanceof InvocationTargetException) { + return new RuntimeException(((InvocationTargetException) e).getTargetException()); + } else if (e instanceof RuntimeException) { + return (RuntimeException) e; + } else { + return new RuntimeException(e); + } + } + + /** + * 代理异常解包 + * + * @param wrapped 包装过得异常 + * @return 解包后的异常 + */ + public static Throwable unwrap(Throwable wrapped) { + Throwable unwrapped = wrapped; + while (true) { + if (unwrapped instanceof InvocationTargetException) { + unwrapped = ((InvocationTargetException) unwrapped).getTargetException(); + } else if (unwrapped instanceof UndeclaredThrowableException) { + unwrapped = ((UndeclaredThrowableException) unwrapped).getUndeclaredThrowable(); + } else { + return unwrapped; + } + } + } + + /** + * 将ErrorStack转化为String. + * + * @param ex Throwable + * @return {String} + */ + public static String getStackTraceAsString(Throwable ex) { + FastStringWriter stringWriter = new FastStringWriter(); + ex.printStackTrace(new PrintWriter(stringWriter)); + return stringWriter.toString(); + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/utils/Func.java b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/Func.java new file mode 100644 index 0000000..8320031 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/Func.java @@ -0,0 +1,1412 @@ +/** + * Copyright (c) 2018-2028, DreamLu 卢春梦 (qq596392912@gmail.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.tool.utils; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import org.springblade.core.tool.date.ConcurrentDateFormat; +import org.springblade.core.tool.date.DateUtil; +import org.springblade.core.tool.jackson.JsonUtil; +import org.springframework.beans.BeansException; +import org.springframework.core.MethodParameter; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; +import org.springframework.web.method.HandlerMethod; + +import java.io.Closeable; +import java.io.InputStream; +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.nio.charset.Charset; +import java.time.Duration; +import java.time.format.DateTimeFormatter; +import java.time.temporal.Temporal; +import java.time.temporal.TemporalAccessor; +import java.util.*; +import java.util.function.Supplier; + +/** + * 工具包集合,只做简单的调用,不删除原有工具类 + * + * @author L.cm + */ +public class Func { + + /** + * Checks that the specified object reference is not {@code null}. This + * method is designed primarily for doing parameter validation in methods + * and constructors, as demonstrated below: + *

+	 * public Foo(Bar bar) {
+	 *     this.bar = $.requireNotNull(bar);
+	 * }
+	 * 
+ * + * @param obj the object reference to check for nullity + * @param the type of the reference + * @return {@code obj} if not {@code null} + * @throws NullPointerException if {@code obj} is {@code null} + */ + public static T requireNotNull(T obj) { + return Objects.requireNonNull(obj); + } + + /** + * Checks that the specified object reference is not {@code null} and + * throws a customized {@link NullPointerException} if it is. This method + * is designed primarily for doing parameter validation in methods and + * constructors with multiple parameters, as demonstrated below: + *
+	 * public Foo(Bar bar, Baz baz) {
+	 *     this.bar = $.requireNotNull(bar, "bar must not be null");
+	 *     this.baz = $.requireNotNull(baz, "baz must not be null");
+	 * }
+	 * 
+ * + * @param obj the object reference to check for nullity + * @param message detail message to be used in the event that a {@code + * NullPointerException} is thrown + * @param the type of the reference + * @return {@code obj} if not {@code null} + * @throws NullPointerException if {@code obj} is {@code null} + */ + public static T requireNotNull(T obj, String message) { + return Objects.requireNonNull(obj, message); + } + + /** + * Checks that the specified object reference is not {@code null} and + * throws a customized {@link NullPointerException} if it is. + * + *

Unlike the method {@link #requireNotNull(Object, String)}, + * this method allows creation of the message to be deferred until + * after the null check is made. While this may confer a + * performance advantage in the non-null case, when deciding to + * call this method care should be taken that the costs of + * creating the message supplier are less than the cost of just + * creating the string message directly. + * + * @param obj the object reference to check for nullity + * @param messageSupplier supplier of the detail message to be + * used in the event that a {@code NullPointerException} is thrown + * @param the type of the reference + * @return {@code obj} if not {@code null} + * @throws NullPointerException if {@code obj} is {@code null} + * @since 1.8 + */ + public static T requireNotNull(T obj, Supplier messageSupplier) { + return Objects.requireNonNull(obj, messageSupplier); + } + + /** + * Returns {@code true} if the provided reference is {@code null} otherwise + * returns {@code false}. + * + * This method exists to be used as a + * {@link java.util.function.Predicate}, {@code filter($::isNull)} + * + * @param obj a reference to be checked against {@code null} + * @return {@code true} if the provided reference is {@code null} otherwise + * {@code false} + * + * @see java.util.function.Predicate + * @since 1.8 + */ + public static boolean isNull(@Nullable Object obj) { + return Objects.isNull(obj); + } + + /** + * Returns {@code true} if the provided reference is non-{@code null} + * otherwise returns {@code false}. + * + * This method exists to be used as a + * {@link java.util.function.Predicate}, {@code filter($::notNull)} + * + * @param obj a reference to be checked against {@code null} + * @return {@code true} if the provided reference is non-{@code null} + * otherwise {@code false} + * + * @see java.util.function.Predicate + * @since 1.8 + */ + public static boolean notNull(@Nullable Object obj) { + return Objects.nonNull(obj); + } + + /** + * 首字母变小写 + * @param str 字符串 + * @return {String} + */ + public static String firstCharToLower(String str) { + return StringUtil.lowerFirst(str); + } + + /** + * 首字母变大写 + * @param str 字符串 + * @return {String} + */ + public static String firstCharToUpper(String str) { + return StringUtil.upperFirst(str); + } + + /** + * Check whether the given {@code CharSequence} contains actual text. + *

More specifically, this method returns {@code true} if the + * {@code CharSequence} is not {@code null}, its length is greater than + * 0, and it contains at least one non-whitespace character. + *

+	 * $.isBlank(null)		= true
+	 * $.isBlank("")		= true
+	 * $.isBlank(" ")		= true
+	 * $.isBlank("12345")	= false
+	 * $.isBlank(" 12345 ")	= false
+	 * 
+ * + * @param cs the {@code CharSequence} to check (may be {@code null}) + * @return {@code true} if the {@code CharSequence} is not {@code null}, + * its length is greater than 0, and it does not contain whitespace only + * @see Character#isWhitespace + */ + public static boolean isBlank(@Nullable final CharSequence cs) { + return StringUtil.isBlank(cs); + } + + /** + *

Checks if a CharSequence is not empty (""), not null and not whitespace only.

+ *
+	 * $.isNotBlank(null)	= false
+	 * $.isNotBlank("")		= false
+	 * $.isNotBlank(" ")	= false
+	 * $.isNotBlank("bob")	= true
+	 * $.isNotBlank("  bob  ") = true
+	 * 
+ * + * @param cs the CharSequence to check, may be null + * @return {@code true} if the CharSequence is + * not empty and not null and not whitespace + * @see Character#isWhitespace + */ + public static boolean isNotBlank(@Nullable final CharSequence cs) { + return StringUtil.isNotBlank(cs); + } + + /** + * 有 任意 一个 Blank + * @param css CharSequence + * @return boolean + */ + public static boolean isAnyBlank(final CharSequence... css) { + return StringUtil.isAnyBlank(css); + } + + /** + * 是否全非 Blank + * @param css CharSequence + * @return boolean + */ + public static boolean isNoneBlank(final CharSequence... css) { + return StringUtil.isNoneBlank(css); + } + + /** + * Determine whether the given object is an array: + * either an Object array or a primitive array. + * @param obj the object to check + * @return 是否数组 + */ + public static boolean isArray(@Nullable Object obj) { + return ObjectUtil.isArray(obj); + } + + /** + * Determine whether the given object is empty: + * i.e. {@code null} or of zero length. + * @param obj the object to check + * @return 数组是否为空 + */ + public static boolean isEmpty(@Nullable Object obj) { + return ObjectUtil.isEmpty(obj); + } + + /** + * Determine whether the given object is not empty: + * i.e. {@code null} or of zero length. + * @param obj the object to check + * @return 是否不为空 + */ + public static boolean isNotEmpty(@Nullable Object obj) { + return !ObjectUtil.isEmpty(obj); + } + + /** + * Determine whether the given array is empty: + * i.e. {@code null} or of zero length. + * @param array the array to check + * @return 数组是否为空 + */ + public static boolean isEmpty(@Nullable Object[] array) { + return ObjectUtil.isEmpty(array); + } + /** + * 判断数组不为空 + * @param array 数组 + * @return 数组是否不为空 + */ + public static boolean isNotEmpty(@Nullable Object[] array) { + return ObjectUtil.isNotEmpty(array); + } + + /** + * 对象组中是否存在 Empty Object + * + * @param os + * 对象组 + * @return + */ + public static boolean hasEmpty(Object... os) { + for (Object o : os) { + if (isEmpty(o)) { + return true; + } + } + return false; + } + + /** + * 对象组中是否全是 Empty Object + * + * @param os + * @return + */ + public static boolean allEmpty(Object... os) { + for (Object o : os) { + if (!isEmpty(o)) { + return false; + } + } + return true; + } + + /** + * 比较两个对象是否相等。
+ * 相同的条件有两个,满足其一即可:
+ * 1. obj1 == null && obj2 == null; 2. obj1.equals(obj2) + * + * @param obj1 + * 对象1 + * @param obj2 + * 对象2 + * @return 是否相等 + */ + public static boolean equals(Object obj1, Object obj2) { + return (obj1 != null) ? (obj1.equals(obj2)) : (obj2 == null); + } + + /** + * Determine if the given objects are equal, returning {@code true} if + * both are {@code null} or {@code false} if only one is {@code null}. + *

Compares arrays with {@code Arrays.equals}, performing an equality + * check based on the array elements rather than the array reference. + * @param o1 first Object to compare + * @param o2 second Object to compare + * @return whether the given objects are equal + * @see Object#equals(Object) + * @see Arrays#equals + */ + public static boolean equalsSafe(@Nullable Object o1, @Nullable Object o2) { + return ObjectUtil.nullSafeEquals(o1, o2); + } + + /** + * Check whether the given Array contains the given element. + * @param array the Array to check + * @param element the element to look for + * @param The generic tag + * @return {@code true} if found, {@code false} else + */ + public static boolean contains(@Nullable T[] array, final T element) { + return CollectionUtil.contains(array, element); + } + + /** + * Check whether the given Iterator contains the given element. + * @param iterator the Iterator to check + * @param element the element to look for + * @return {@code true} if found, {@code false} otherwise + */ + public static boolean contains(@Nullable Iterator iterator, Object element) { + return CollectionUtil.contains(iterator, element); + } + + /** + * Check whether the given Enumeration contains the given element. + * @param enumeration the Enumeration to check + * @param element the element to look for + * @return {@code true} if found, {@code false} otherwise + */ + public static boolean contains(@Nullable Enumeration enumeration, Object element) { + return CollectionUtil.contains(enumeration, element); + } + + /** + * 强转->string,并去掉多余空格 + * + * @param str + * @return + */ + public static String toStr(Object str) { + return toStr(str, ""); + } + + /** + * 强转->string,并去掉多余空格 + * + * @param str + * @param defaultValue + * @return + */ + public static String toStr(Object str, String defaultValue) { + if (null == str) { + return defaultValue; + } + return String.valueOf(str); + } + + /** + * 判断一个字符串是否是数字 + * @param cs the CharSequence to check, may be null + * @return {boolean} + */ + public static boolean isNumeric(final CharSequence cs) { + return StringUtil.isNumeric(cs); + } + + /** + *

Convert a String to an int, returning + * zero if the conversion fails.

+ * + *

If the string is null, zero is returned.

+ * + *
+	 *   $.toInt(null) = 0
+	 *   $.toInt("")   = 0
+	 *   $.toInt("1")  = 1
+	 * 
+ * + * @param value the string to convert, may be null + * @return the int represented by the string, or zero if + * conversion fails + */ + public static int toInt(final Object value) { + return NumberUtil.toInt(String.valueOf(value)); + } + + /** + *

Convert a String to an int, returning a + * default value if the conversion fails.

+ * + *

If the string is null, the default value is returned.

+ * + *
+	 *   $.toInt(null, 1) = 1
+	 *   $.toInt("", 1)   = 1
+	 *   $.toInt("1", 0)  = 1
+	 * 
+ * + * @param value the string to convert, may be null + * @param defaultValue the default value + * @return the int represented by the string, or the default if conversion fails + */ + public static int toInt(@Nullable final Object value, final int defaultValue) { + return NumberUtil.toInt(String.valueOf(value), defaultValue); + } + + /** + *

Convert a String to a long, returning + * zero if the conversion fails.

+ * + *

If the string is null, zero is returned.

+ * + *
+	 *   $.toLong(null) = 0L
+	 *   $.toLong("")   = 0L
+	 *   $.toLong("1")  = 1L
+	 * 
+ * + * @param value the string to convert, may be null + * @return the long represented by the string, or 0 if + * conversion fails + */ + public static long toLong(final Object value) { + return NumberUtil.toLong(String.valueOf(value)); + } + + /** + *

Convert a String to a long, returning a + * default value if the conversion fails.

+ * + *

If the string is null, the default value is returned.

+ * + *
+	 *   $.toLong(null, 1L) = 1L
+	 *   $.toLong("", 1L)   = 1L
+	 *   $.toLong("1", 0L)  = 1L
+	 * 
+ * + * @param value the string to convert, may be null + * @param defaultValue the default value + * @return the long represented by the string, or the default if conversion fails + */ + public static long toLong(@Nullable final Object value, final long defaultValue) { + return NumberUtil.toLong(String.valueOf(value), defaultValue); + } + + /** + *

Convert a String to an Double, returning a + * default value if the conversion fails.

+ * + *

If the string is null, the default value is returned.

+ * + *
+	 *   $.toDouble(null, 1) = 1.0
+	 *   $.toDouble("", 1)   = 1.0
+	 *   $.toDouble("1", 0)  = 1.0
+	 * 
+ * + * @param value the string to convert, may be null + * @return the int represented by the string, or the default if conversion fails + */ + public static Double toDouble(Object value) { + return toDouble(String.valueOf(value), -1.00); + } + + /** + *

Convert a String to an Double, returning a + * default value if the conversion fails.

+ * + *

If the string is null, the default value is returned.

+ * + *
+	 *   $.toDouble(null, 1) = 1.0
+	 *   $.toDouble("", 1)   = 1.0
+	 *   $.toDouble("1", 0)  = 1.0
+	 * 
+ * + * @param value the string to convert, may be null + * @param defaultValue the default value + * @return the int represented by the string, or the default if conversion fails + */ + public static Double toDouble(Object value, Double defaultValue) { + return NumberUtil.toDouble(String.valueOf(value), defaultValue); + } + + /** + *

Convert a String to an Float, returning a + * default value if the conversion fails.

+ * + *

If the string is null, the default value is returned.

+ * + *
+	 *   $.toFloat(null, 1) = 1.00f
+	 *   $.toFloat("", 1)   = 1.00f
+	 *   $.toFloat("1", 0)  = 1.00f
+	 * 
+ * + * @param value the string to convert, may be null + * @return the int represented by the string, or the default if conversion fails + */ + public static Float toFloat(Object value) { + return toFloat(String.valueOf(value), -1.0f); + } + + /** + *

Convert a String to an Float, returning a + * default value if the conversion fails.

+ * + *

If the string is null, the default value is returned.

+ * + *
+	 *   $.toFloat(null, 1) = 1.00f
+	 *   $.toFloat("", 1)   = 1.00f
+	 *   $.toFloat("1", 0)  = 1.00f
+	 * 
+ * + * @param value the string to convert, may be null + * @param defaultValue the default value + * @return the int represented by the string, or the default if conversion fails + */ + public static Float toFloat(Object value, Float defaultValue) { + return NumberUtil.toFloat(String.valueOf(value), defaultValue); + } + + /** + *

Convert a String to an Boolean, returning a + * default value if the conversion fails.

+ * + *

If the string is null, the default value is returned.

+ * + *
+	 *   $.toBoolean("true", true)  = true
+	 *   $.toBoolean("false")   	= false
+	 *   $.toBoolean("", false)  	= false
+	 * 
+ * + * @param value the string to convert, may be null + * @return the int represented by the string, or the default if conversion fails + */ + public static Boolean toBoolean(Object value) { + return toBoolean(value, null); + } + + /** + *

Convert a String to an Boolean, returning a + * default value if the conversion fails.

+ * + *

If the string is null, the default value is returned.

+ * + *
+	 *   $.toBoolean("true", true)  = true
+	 *   $.toBoolean("false")   	= false
+	 *   $.toBoolean("", false)  	= false
+	 * 
+ * + * @param value the string to convert, may be null + * @param defaultValue the default value + * @return the int represented by the string, or the default if conversion fails + */ + public static Boolean toBoolean(Object value, Boolean defaultValue) { + if (value != null) { + String val = String.valueOf(value); + val = val.toLowerCase().trim(); + if ("true".equalsIgnoreCase(val)) { + return true; + } else if ("false".equalsIgnoreCase(val)) { + return false; + } + throw new RuntimeException("The value can not parse to Boolean : " + val); + } + return defaultValue; + } + + /** + * 转换为Integer数组
+ * + * @param str 被转换的值 + * @return 结果 + */ + public static Integer[] toIntArray(String str) { + return toIntArray(",", str); + } + + /** + * 转换为Integer数组
+ * + * @param split 分隔符 + * @param split 被转换的值 + * @return 结果 + */ + public static Integer[] toIntArray(String split, String str) { + if (StringUtil.isEmpty(str)) { + return new Integer[] {}; + } + String[] arr = str.split(split); + final Integer[] ints = new Integer[arr.length]; + for (int i = 0; i < arr.length; i++) { + final Integer v = toInt(arr[i], 0); + ints[i] = v; + } + return ints; + } + + /** + * 转换为String数组
+ * + * @param str 被转换的值 + * @return 结果 + */ + public static String[] toStrArray(String str) { + return toStrArray(",", str); + } + + /** + * 转换为String数组
+ * + * @param split 分隔符 + * @param split 被转换的值 + * @return 结果 + */ + public static String[] toStrArray(String split, String str) { + if (isBlank(str)) { + return new String[]{}; + } + return str.split(split); + } + + /** + * 将 long 转短字符串 为 62 进制 + * @param num 数字 + * @return 短字符串 + */ + public static String to62String(long num) { + return NumberUtil.to62String(num); + } + + /** + * Convert a {@code Collection} into a delimited {@code String} (e.g., CSV). + *

Useful for {@code toString()} implementations. + * + * @param coll the {@code Collection} to convert + * @return the delimited {@code String} + */ + public static String join(Collection coll) { + return StringUtil.join(coll); + } + + /** + * Convert a {@code Collection} into a delimited {@code String} (e.g. CSV). + *

Useful for {@code toString()} implementations. + * + * @param coll the {@code Collection} to convert + * @param delim the delimiter to use (typically a ",") + * @return the delimited {@code String} + */ + public static String join(Collection coll, String delim) { + return StringUtil.join(coll, delim); + } + + /** + * Convert a {@code String} array into a comma delimited {@code String} + * (i.e., CSV). + *

Useful for {@code toString()} implementations. + * + * @param arr the array to display + * @return the delimited {@code String} + */ + public static String join(Object[] arr) { + return StringUtil.join(arr); + } + + /** + * Convert a {@code String} array into a delimited {@code String} (e.g. CSV). + *

Useful for {@code toString()} implementations. + * + * @param arr the array to display + * @param delim the delimiter to use (typically a ",") + * @return the delimited {@code String} + */ + public static String join(Object[] arr, String delim) { + return StringUtil.join(arr, delim); + } + + /** + * 生成uuid + * + * @return UUID + */ + public static String randomUUID() { + return StringUtil.randomUUID(); + } + + /** + * 转义HTML用于安全过滤 + * + * @param html html + * @return {String} + */ + public static String escapeHtml(String html) { + return StringUtil.escapeHtml(html); + } + + /** + * 随机数生成 + * + * @param count 字符长度 + * @return 随机数 + */ + public static String random(int count) { + return StringUtil.random(count); + } + + /** + * 随机数生成 + * + * @param count 字符长度 + * @param randomType 随机数类别 + * @return 随机数 + */ + public static String random(int count, RandomType randomType) { + return StringUtil.random(count, randomType); + } + + /** + * Calculates the MD5 digest and returns the value as a 32 character hex string. + * + * @param data Data to digest + * @return MD5 digest as a hex string + */ + public static String md5Hex(final String data) { + return DigestUtil.md5Hex(data); + } + + /** + * Return a hexadecimal string representation of the MD5 digest of the given bytes. + * + * @param bytes the bytes to calculate the digest over + * @return a hexadecimal digest string + */ + public static String md5Hex(final byte[] bytes) { + return DigestUtil.md5Hex(bytes); + } + + public static String sha1(String srcStr) { + return DigestUtil.sha1(srcStr); + } + + public static String sha256(String srcStr) { + return DigestUtil.sha256(srcStr); + } + + public static String sha384(String srcStr) { + return DigestUtil.sha384(srcStr); + } + + public static String sha512(String srcStr) { + return DigestUtil.sha512(srcStr); + } + + /** + * 自定义加密 先MD5再SHA1 + * @param data + * @return + */ + public static String encrypt(String data) { + return DigestUtil.encrypt(data); + } + + /** + * 编码 + * @param value 字符串 + * @return {String} + */ + public static String encodeBase64(String value) { + return Base64Util.encode(value); + } + + /** + * 编码 + * @param value 字符串 + * @param charset 字符集 + * @return {String} + */ + public static String encodeBase64(String value, Charset charset) { + return Base64Util.encode(value, charset); + } + + /** + * 编码URL安全 + * @param value 字符串 + * @return {String} + */ + public static String encodeBase64UrlSafe(String value) { + return Base64Util.encodeUrlSafe(value); + } + + /** + * 编码URL安全 + * @param value 字符串 + * @param charset 字符集 + * @return {String} + */ + public static String encodeBase64UrlSafe(String value, Charset charset) { + return Base64Util.encodeUrlSafe(value, charset); + } + + /** + * 解码 + * @param value 字符串 + * @return {String} + */ + public static String decodeBase64(String value) { + return Base64Util.decode(value); + } + + /** + * 解码 + * @param value 字符串 + * @param charset 字符集 + * @return {String} + */ + public static String decodeBase64(String value, Charset charset) { + return Base64Util.decode(value, charset); + } + + /** + * 解码URL安全 + * @param value 字符串 + * @return {String} + */ + public static String decodeBase64UrlSafe(String value) { + return Base64Util.decodeUrlSafe(value); + } + + /** + * 解码URL安全 + * @param value 字符串 + * @param charset 字符集 + * @return {String} + */ + public static String decodeBase64UrlSafe(String value, Charset charset) { + return Base64Util.decodeUrlSafe(value, charset); + } + + /** + * closeQuietly + * + * @param closeable 自动关闭 + */ + public static void closeQuietly(@Nullable Closeable closeable) { + IOUtil.closeQuietly(closeable); + } + + /** + * InputStream to String utf-8 + * + * @param input the InputStream to read from + * @return the requested String + * @throws NullPointerException if the input is null + */ + public static String toString(InputStream input) { + return IOUtil.toString(input); + } + + /** + * InputStream to String + * + * @param input the InputStream to read from + * @param charset the Charsets + * @return the requested String + * @throws NullPointerException if the input is null + */ + public static String toString(@Nullable InputStream input, Charset charset) { + return IOUtil.toString(input, charset); + } + + public static byte[] toByteArray(@Nullable InputStream input) { + return IOUtil.toByteArray(input); + } + + /** + * 将对象序列化成json字符串 + * + * @param object javaBean + * @return jsonString json字符串 + */ + public static String toJson(Object object) { + return JsonUtil.toJson(object); + } + + /** + * 将对象序列化成 json byte 数组 + * + * @param object javaBean + * @return jsonString json字符串 + */ + public static byte[] toJsonAsBytes(Object object) { + return JsonUtil.toJsonAsBytes(object); + } + + /** + * 将json字符串转成 JsonNode + * + * @param jsonString jsonString + * @return jsonString json字符串 + */ + public static JsonNode readTree(String jsonString) { + return JsonUtil.readTree(jsonString); + } + + /** + * 将json字符串转成 JsonNode + * + * @param in InputStream + * @return jsonString json字符串 + */ + public static JsonNode readTree(InputStream in) { + return JsonUtil.readTree(in); + } + + /** + * 将json字符串转成 JsonNode + * + * @param content content + * @return jsonString json字符串 + */ + public static JsonNode readTree(byte[] content) { + return JsonUtil.readTree(content); + } + + /** + * 将json字符串转成 JsonNode + * + * @param jsonParser JsonParser + * @return jsonString json字符串 + */ + public static JsonNode readTree(JsonParser jsonParser) { + return JsonUtil.readTree(jsonParser); + } + + /** + * 将json byte 数组反序列化成对象 + * + * @param bytes json bytes + * @param valueType class + * @param T 泛型标记 + * @return Bean + */ + public static T parse(byte[] bytes, Class valueType) { + return JsonUtil.parse(bytes, valueType); + } + + /** + * 将json反序列化成对象 + * + * @param jsonString jsonString + * @param valueType class + * @param T 泛型标记 + * @return Bean + */ + public static T parse(String jsonString, Class valueType) { + return JsonUtil.parse(jsonString, valueType); + } + + /** + * 将json反序列化成对象 + * + * @param in InputStream + * @param valueType class + * @param T 泛型标记 + * @return Bean + */ + public static T parse(InputStream in, Class valueType) { + return JsonUtil.parse(in, valueType); + } + + /** + * 将json反序列化成对象 + * + * @param bytes bytes + * @param typeReference 泛型类型 + * @param T 泛型标记 + * @return Bean + */ + public static T parse(byte[] bytes, TypeReference typeReference) { + return JsonUtil.parse(bytes, typeReference); + } + + /** + * 将json反序列化成对象 + * + * @param jsonString jsonString + * @param typeReference 泛型类型 + * @param T 泛型标记 + * @return Bean + */ + public static T parse(String jsonString, TypeReference typeReference) { + return JsonUtil.parse(jsonString, typeReference); + } + + /** + * 将json反序列化成对象 + * + * @param in InputStream + * @param typeReference 泛型类型 + * @param T 泛型标记 + * @return Bean + */ + public static T parse(InputStream in, TypeReference typeReference) { + return JsonUtil.parse(in, typeReference); + } + + /** + * Encode all characters that are either illegal, or have any reserved + * meaning, anywhere within a URI, as defined in + * RFC 3986. + * This is useful to ensure that the given String will be preserved as-is + * and will not have any o impact on the structure or meaning of the URI. + * @param source the String to be encoded + * @return the encoded String + */ + public static String encode(String source) { + return URLUtil.encode(source, Charsets.UTF_8); + } + + /** + * Encode all characters that are either illegal, or have any reserved + * meaning, anywhere within a URI, as defined in + * RFC 3986. + * This is useful to ensure that the given String will be preserved as-is + * and will not have any o impact on the structure or meaning of the URI. + * @param source the String to be encoded + * @param charset the character encoding to encode to + * @return the encoded String + */ + public static String encode(String source, Charset charset) { + return URLUtil.encode(source, charset); + } + + /** + * Decode the given encoded URI component. + *

See {@link StringUtils#uriDecode(String, Charset)} for the decoding rules. + * @param source the encoded String + * @return the decoded value + * @throws IllegalArgumentException when the given source contains invalid encoded sequences + * @see StringUtils#uriDecode(String, Charset) + * @see java.net.URLDecoder#decode(String, String) + */ + public static String decode(String source) { + return StringUtils.uriDecode(source, Charsets.UTF_8); + } + + /** + * Decode the given encoded URI component. + *

See {@link StringUtils#uriDecode(String, Charset)} for the decoding rules. + * @param source the encoded String + * @param charset the character encoding to use + * @return the decoded value + * @throws IllegalArgumentException when the given source contains invalid encoded sequences + * @see StringUtils#uriDecode(String, Charset) + * @see java.net.URLDecoder#decode(String, String) + */ + public static String decode(String source, Charset charset) { + return StringUtils.uriDecode(source, charset); + } + + /** + * 日期时间格式化 + * + * @param date 时间 + * @return 格式化后的时间 + */ + public static String formatDateTime(Date date) { + return DateUtil.formatDateTime(date); + } + + /** + * 日期格式化 + * + * @param date 时间 + * @return 格式化后的时间 + */ + public static String formatDate(Date date) { + return DateUtil.formatDate(date); + } + + /** + * 时间格式化 + * + * @param date 时间 + * @return 格式化后的时间 + */ + public static String formatTime(Date date) { + return DateUtil.formatTime(date); + } + + /** + * 日期格式化 + * + * @param date 时间 + * @param pattern 表达式 + * @return 格式化后的时间 + */ + public static String format(Date date, String pattern) { + return DateUtil.format(date, pattern); + } + + /** + * 将字符串转换为时间 + * + * @param dateStr 时间字符串 + * @param pattern 表达式 + * @return 时间 + */ + public static Date parseDate(String dateStr, String pattern) { + return DateUtil.parse(dateStr, pattern); + } + + /** + * 将字符串转换为时间 + * + * @param dateStr 时间字符串 + * @param format ConcurrentDateFormat + * @return 时间 + */ + public static Date parse(String dateStr, ConcurrentDateFormat format) { + return DateUtil.parse(dateStr, format); + } + + /** + * 日期时间格式化 + * + * @param temporal 时间 + * @return 格式化后的时间 + */ + public static String formatDateTime(TemporalAccessor temporal) { + return DateTimeUtil.formatDateTime(temporal); + } + + /** + * 日期时间格式化 + * + * @param temporal 时间 + * @return 格式化后的时间 + */ + public static String formatDate(TemporalAccessor temporal) { + return DateTimeUtil.formatDate(temporal); + } + + /** + * 时间格式化 + * + * @param temporal 时间 + * @return 格式化后的时间 + */ + public static String formatTime(TemporalAccessor temporal) { + return DateTimeUtil.formatTime(temporal); + } + + /** + * 日期格式化 + * + * @param temporal 时间 + * @param pattern 表达式 + * @return 格式化后的时间 + */ + public static String format(TemporalAccessor temporal, String pattern) { + return DateTimeUtil.format(temporal, pattern); + } + + /** + * 将字符串转换为时间 + * + * @param dateStr 时间字符串 + * @param pattern 表达式 + * @return 时间 + */ + public static TemporalAccessor parse(String dateStr, String pattern) { + return DateTimeUtil.parse(dateStr, pattern); + } + + /** + * 将字符串转换为时间 + * + * @param dateStr 时间字符串 + * @param formatter DateTimeFormatter + * @return 时间 + */ + public static TemporalAccessor parse(String dateStr, DateTimeFormatter formatter) { + return DateTimeUtil.parse(dateStr, formatter); + } + + /** + * 时间比较 + * + * @param startInclusive the start instant, inclusive, not null + * @param endExclusive the end instant, exclusive, not null + * @return a {@code Duration}, not null + */ + public static Duration between(Temporal startInclusive, Temporal endExclusive) { + return Duration.between(startInclusive, endExclusive); + } + + /** + * 获取方法参数信息 + * @param constructor 构造器 + * @param parameterIndex 参数序号 + * @return {MethodParameter} + */ + public static MethodParameter getMethodParameter(Constructor constructor, int parameterIndex) { + return ClassUtil.getMethodParameter(constructor, parameterIndex); + } + + /** + * 获取方法参数信息 + * @param method 方法 + * @param parameterIndex 参数序号 + * @return {MethodParameter} + */ + public static MethodParameter getMethodParameter(Method method, int parameterIndex) { + return ClassUtil.getMethodParameter(method, parameterIndex); + } + + /** + * 获取Annotation + * @param annotatedElement AnnotatedElement + * @param annotationType 注解类 + * @param 泛型标记 + * @return {Annotation} + */ + @Nullable + public static A getAnnotation(AnnotatedElement annotatedElement, Class annotationType) { + return AnnotatedElementUtils.findMergedAnnotation(annotatedElement, annotationType); + } + + /** + * 获取Annotation + * @param method Method + * @param annotationType 注解类 + * @param 泛型标记 + * @return {Annotation} + */ + @Nullable + public static A getAnnotation(Method method, Class annotationType) { + return ClassUtil.getAnnotation(method, annotationType); + } + + /** + * 获取Annotation + * @param handlerMethod HandlerMethod + * @param annotationType 注解类 + * @param 泛型标记 + * @return {Annotation} + */ + @Nullable + public static A getAnnotation(HandlerMethod handlerMethod, Class annotationType) { + return ClassUtil.getAnnotation(handlerMethod, annotationType); + } + + /** + * 实例化对象 + * @param clazz 类 + * @param 泛型标记 + * @return 对象 + */ + @SuppressWarnings("unchecked") + public static T newInstance(Class clazz) { + return (T) BeanUtil.instantiateClass(clazz); + } + + /** + * 实例化对象 + * @param clazzStr 类名 + * @param 泛型标记 + * @return 对象 + */ + public static T newInstance(String clazzStr) { + return BeanUtil.newInstance(clazzStr); + } + + /** + * 获取Bean的属性 + * @param bean bean + * @param propertyName 属性名 + * @return 属性值 + */ + public static Object getProperty(Object bean, String propertyName) { + return BeanUtil.getProperty(bean, propertyName); + } + + /** + * 设置Bean属性 + * @param bean bean + * @param propertyName 属性名 + * @param value 属性值 + */ + public static void setProperty(Object bean, String propertyName, Object value) { + BeanUtil.setProperty(bean, propertyName, value); + } + + /** + * 深复制 + * + * 注意:不支持链式Bean + * + * @param source 源对象 + * @param 泛型标记 + * @return T + */ + public static T clone(T source) { + return BeanUtil.clone(source); + } + + /** + * copy 对象属性到另一个对象,默认不使用Convert + * + * 注意:不支持链式Bean,链式用 copyProperties + * + * @param source 源对象 + * @param clazz 类名 + * @param 泛型标记 + * @return T + */ + public static T copy(Object source, Class clazz) { + return BeanUtil.copy(source, clazz); + } + + /** + * 拷贝对象 + * + * 注意:不支持链式Bean,链式用 copyProperties + * + * @param source 源对象 + * @param targetBean 需要赋值的对象 + */ + public static void copy(Object source, Object targetBean) { + BeanUtil.copy(source, targetBean); + } + + /** + * Copy the property values of the given source bean into the target class. + *

Note: The source and target classes do not have to match or even be derived + * from each other, as long as the properties match. Any bean properties that the + * source bean exposes but the target bean does not will silently be ignored. + *

This is just a convenience method. For more complex transfer needs, + * @param source the source bean + * @param clazz the target bean class + * @param 泛型标记 + * @throws BeansException if the copying failed + * @return T + */ + public static T copyProperties(Object source, Class clazz) throws BeansException { + return BeanUtil.copyProperties(source, clazz); + } + + /** + * 将对象装成map形式 + * @param bean 源对象 + * @return {Map} + */ + public static Map toMap(Object bean) { + return BeanUtil.toMap(bean); + } + /** + * 将map 转为 bean + * @param beanMap map + * @param valueType 对象类型 + * @param 泛型标记 + * @return {T} + */ + public static T toBean(Map beanMap, Class valueType) { + return BeanUtil.toBean(beanMap, valueType); + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/utils/IOUtil.java b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/IOUtil.java new file mode 100644 index 0000000..84be0f0 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/IOUtil.java @@ -0,0 +1,100 @@ +/** + * Copyright (c) 2018-2028, DreamLu 卢春梦 (qq596392912@gmail.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.tool.utils; + +import org.springframework.lang.Nullable; + +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * IOUtil + * + * @author L.cm + */ +public class IOUtil extends org.springframework.util.StreamUtils { + + /** + * closeQuietly + * + * @param closeable 自动关闭 + */ + public static void closeQuietly(@Nullable Closeable closeable) { + try { + if (closeable != null) { + closeable.close(); + } + } catch (IOException ioe) { + // ignore + } + } + + /** + * InputStream to String utf-8 + * + * @param input the InputStream to read from + * @return the requested String + */ + public static String toString(InputStream input) { + return toString(input, Charsets.UTF_8); + } + + /** + * InputStream to String + * + * @param input the InputStream to read from + * @param charset the Charsets + * @return the requested String + */ + public static String toString(@Nullable InputStream input, java.nio.charset.Charset charset) { + try { + return IOUtil.copyToString(input, charset); + } catch (IOException e) { + throw Exceptions.unchecked(e); + } finally { + IOUtil.closeQuietly(input); + } + } + + public static byte[] toByteArray(@Nullable InputStream input) { + try { + return IOUtil.copyToByteArray(input); + } catch (IOException e) { + throw Exceptions.unchecked(e); + } finally { + IOUtil.closeQuietly(input); + } + } + + /** + * Writes chars from a String to bytes on an + * OutputStream using the specified character encoding. + *

+ * This method uses {@link String#getBytes(String)}. + * + * @param data the String to write, null ignored + * @param output the OutputStream to write to + * @param encoding the encoding to use, null means platform default + * @throws IOException if an I/O error occurs + */ + public static void write(@Nullable final String data, final OutputStream output, final java.nio.charset.Charset encoding) throws IOException { + if (data != null) { + output.write(data.getBytes(encoding)); + } + } +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/utils/ImageUtil.java b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/ImageUtil.java new file mode 100644 index 0000000..e5cb753 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/ImageUtil.java @@ -0,0 +1,428 @@ + +package org.springblade.core.tool.utils; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springblade.core.tool.support.IMultiOutputStream; +import org.springblade.core.tool.support.ImagePosition; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.color.ColorSpace; +import java.awt.geom.AffineTransform; +import java.awt.image.*; +import java.io.*; +import java.net.URL; + +public final class ImageUtil { + + /** + * Logger for this class + */ + private static Logger LOGGER = LoggerFactory.getLogger(ImageUtil.class); + + /** 默认输出图片类型 */ + public static final String DEFAULT_IMG_TYPE = "JPEG"; + + private ImageUtil() { + + } + + /** + * 转换输入流到byte + * @param src + * @return + * @throws IOException + */ + public static byte[] toByteArray(BufferedImage src, String type) throws IOException { + ByteArrayOutputStream os = new ByteArrayOutputStream(); + ImageIO.write(src, defaultString(type, DEFAULT_IMG_TYPE), os); + return os.toByteArray(); + } + + /** + * 获取图像内容 + * @param srcImageFile 文件路径 + * @return + */ + public static BufferedImage readImage(String srcImageFile) { + try { + return ImageIO.read(new File(srcImageFile)); + } catch (IOException e) { + LOGGER.error("Error readImage",e); + } + return null; + } + /** + * 获取图像内容 + * @param srcImageFile 文件 + * @return + */ + public static BufferedImage readImage(File srcImageFile) { + try { + return ImageIO.read(srcImageFile); + } catch (IOException e) { + LOGGER.error("Error readImage",e); + } + return null; + } + /** + * 获取图像内容 + * @param srcInputStream 输入流 + * @return + */ + public static BufferedImage readImage(InputStream srcInputStream) { + try { + return ImageIO.read(srcInputStream); + } catch (IOException e) { + LOGGER.error("Error readImage",e); + } + return null; + } + /** + * 获取图像内容 + * @param url URL地址 + * @return + */ + public static BufferedImage readImage(URL url) { + try { + return ImageIO.read(url); + } catch (IOException e) { + LOGGER.error("Error readImage",e); + } + return null; + } + + + /** + * 缩放图像(按比例缩放) + * @param src 源图像 + * @param output 输出流 + * @param scale 缩放比例 + * @param flag 缩放选择:true 放大; false 缩小; + */ + public final static void zoomScale(BufferedImage src, OutputStream output, String type, double scale, boolean flag) { + try { + // 得到源图宽 + int width = src.getWidth(); + // 得到源图长 + int height = src.getHeight(); + if (flag) { + // 放大 + width = Long.valueOf(Math.round(width * scale)).intValue(); + height = Long.valueOf(Math.round(height * scale)).intValue(); + } else { + // 缩小 + width = Long.valueOf(Math.round(width / scale)).intValue(); + height = Long.valueOf(Math.round(height / scale)).intValue(); + } + Image image = src.getScaledInstance(width, height, Image.SCALE_DEFAULT); + BufferedImage tag = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); + Graphics g = tag.getGraphics(); + // 绘制缩小后的图 + g.drawImage(image, 0, 0, null); + g.dispose(); + // 输出为文件 + ImageIO.write(tag, defaultString(type, DEFAULT_IMG_TYPE), output); + // 关闭流 + output.close(); + } catch (IOException e) { + LOGGER.error("Error in zoom image", e); + } + } + /** + * 缩放图像(按高度和宽度缩放) + * @param src 源图像 + * @param output 输出流 + * @param height 缩放后的高度 + * @param width 缩放后的宽度 + * @param bb 比例不对时是否需要补白:true为补白; false为不补白; + * @param fillColor 填充色,null时为Color.WHITE + */ + public final static void zoomFixed(BufferedImage src, OutputStream output, String type, int height, int width, boolean bb, Color fillColor) { + try { + double ratio = 0.0; // 缩放比例 + Image itemp = src.getScaledInstance(width, height, BufferedImage.SCALE_SMOOTH); + // 计算比例 + if (src.getHeight() > src.getWidth()) { + ratio = Integer.valueOf(height).doubleValue() / src.getHeight(); + } else { + ratio = Integer.valueOf(width).doubleValue() / src.getWidth(); + } + AffineTransformOp op = new AffineTransformOp(AffineTransform.getScaleInstance(ratio, ratio), null); + itemp = op.filter(src, null); + + if (bb) { + //补白 + BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); + Graphics2D g = image.createGraphics(); + Color fill = fillColor == null ? Color.white:fillColor ; + g.setColor(fill); + g.fillRect(0, 0, width, height); + if (width == itemp.getWidth(null)) { + g.drawImage(itemp, 0, (height - itemp.getHeight(null)) / 2, itemp.getWidth(null), itemp.getHeight(null), fill, null); + } else { + g.drawImage(itemp, (width - itemp.getWidth(null)) / 2, 0, itemp.getWidth(null), itemp.getHeight(null), fill, null); + } + g.dispose(); + itemp = image; + } + // 输出为文件 + ImageIO.write((BufferedImage) itemp, defaultString(type, DEFAULT_IMG_TYPE), output); + // 关闭流 + output.close(); + } catch (IOException e) { + LOGGER.error("Error in zoom image", e); + } + } + + /** + * 图像裁剪(按指定起点坐标和宽高切割) + * @param src 源图像 + * @param output 切片后的图像地址 + * @param x 目标切片起点坐标X + * @param y 目标切片起点坐标Y + * @param width 目标切片宽度 + * @param height 目标切片高度 + */ + public final static void crop(BufferedImage src, OutputStream output, String type, int x, int y, int width, int height) { + try { + // 源图宽度 + int srcWidth = src.getHeight(); + // 源图高度 + int srcHeight = src.getWidth(); + if (srcWidth > 0 && srcHeight > 0) { + Image image = src.getScaledInstance(srcWidth, srcHeight, Image.SCALE_DEFAULT); + // 四个参数分别为图像起点坐标和宽高 + ImageFilter cropFilter = new CropImageFilter(x, y, width, height); + Image img = Toolkit.getDefaultToolkit().createImage( new FilteredImageSource(image.getSource(), cropFilter)); + BufferedImage tag = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); + Graphics g = tag.getGraphics(); + g.drawImage(img, 0, 0, width, height, null); // 绘制切割后的图 + g.dispose(); + // 输出为文件 + ImageIO.write(tag, defaultString(type, DEFAULT_IMG_TYPE), output); + // 关闭流 + output.close(); + } + } catch (Exception e) { + LOGGER.error("Error in cut image", e); + } + } + + /** + * 图像切割(指定切片的行数和列数) + * @param src 源图像地址 + * @param mos 切片目标文件夹 + * @param prows 目标切片行数。默认2,必须是范围 [1, 20] 之内 + * @param pcols 目标切片列数。默认2,必须是范围 [1, 20] 之内 + */ + public final static void sliceWithNumber(BufferedImage src, IMultiOutputStream mos, String type, int prows, int pcols) { + try { + int rows = prows<=0|| prows>20 ? 2: prows; + int cols = pcols<=0|| pcols>20 ? 2: pcols; + // 源图宽度 + int srcWidth = src.getHeight(); + // 源图高度 + int srcHeight = src.getWidth(); + if (srcWidth > 0 && srcHeight > 0) { + Image img; + ImageFilter cropFilter; + Image image = src.getScaledInstance(srcWidth, srcHeight, Image.SCALE_DEFAULT); + // 每张切片的宽度 + int destWidth = (srcWidth%cols==0)?(srcWidth/cols):(srcWidth/cols + 1); + // 每张切片的高度 + int destHeight = (srcHeight%rows==0)?(srcHeight/rows):(srcHeight/rows+1); + // 循环建立切片 + // 改进的想法:是否可用多线程加快切割速度 + for (int i = 0; i < rows; i++) { + for (int j = 0; j < cols; j++) { + // 四个参数分别为图像起点坐标和宽高 + cropFilter = new CropImageFilter(j * destWidth, i * destHeight, destWidth, destHeight); + img = Toolkit.getDefaultToolkit().createImage( new FilteredImageSource(image.getSource(), cropFilter)); + BufferedImage tag = new BufferedImage(destWidth, destHeight, BufferedImage.TYPE_INT_RGB); + Graphics g = tag.getGraphics(); + // 绘制缩小后的图 + g.drawImage(img, 0, 0, null); + g.dispose(); + // 输出为文件 + ImageIO.write(tag, defaultString(type, DEFAULT_IMG_TYPE), mos.buildOutputStream(i,j) ); + } + } + } + } catch (Exception e) { + LOGGER.error("Error in slice image", e); + } + } + /** + * 图像切割(指定切片的宽度和高度) + * @param src 源图像地址 + * @param mos 切片目标文件夹 + * @param pdestWidth 目标切片宽度。默认200 + * @param pdestHeight 目标切片高度。默认150 + */ + public final static void sliceWithSize(BufferedImage src, IMultiOutputStream mos, String type, int pdestWidth, int pdestHeight) { + try { + int destWidth = pdestWidth<= 0 ? 200 : pdestWidth; + int destHeight = pdestHeight <= 0 ? 150 : pdestHeight; + // 源图宽度 + int srcWidth = src.getHeight(); + // 源图高度 + int srcHeight = src.getWidth(); + if (srcWidth > destWidth && srcHeight > destHeight) { + Image img; + ImageFilter cropFilter; + Image image = src.getScaledInstance(srcWidth, srcHeight, Image.SCALE_DEFAULT); + // 切片横向数量 + int cols = (srcWidth % destWidth == 0)?(srcWidth/destWidth):(srcWidth/destWidth + 1); + // 切片纵向数量 + int rows = (srcHeight % destHeight == 0)?(srcHeight/destHeight):(srcHeight/destHeight+1); + // 循环建立切片 + // 改进的想法:是否可用多线程加快切割速度 + for (int i = 0; i < rows; i++) { + for (int j = 0; j < cols; j++) { + // 四个参数分别为图像起点坐标和宽高 + cropFilter = new CropImageFilter(j * destWidth, i * destHeight, destWidth, destHeight); + img = Toolkit.getDefaultToolkit().createImage( new FilteredImageSource(image.getSource(), cropFilter)); + BufferedImage tag = new BufferedImage(destWidth, destHeight, BufferedImage.TYPE_INT_RGB); + Graphics g = tag.getGraphics(); + // 绘制缩小后的图 + g.drawImage(img, 0, 0, null); + g.dispose(); + // 输出为文件 + ImageIO.write(tag, defaultString(type, DEFAULT_IMG_TYPE), mos.buildOutputStream(i,j)); + } + } + } + } catch (Exception e) { + LOGGER.error("Error in slice image", e); + } + } + /** + * 图像类型转换:GIF->JPG、GIF->PNG、PNG->JPG、PNG->GIF(X)、BMP->PNG + * @param src 源图像地址 + * @param formatName 包含格式非正式名称的 String:如JPG、JPEG、GIF等 + * @param output 目标图像地址 + */ + public final static void convert(BufferedImage src, OutputStream output, String formatName) { + try { + // 输出为文件 + ImageIO.write(src, formatName, output); + // 关闭流 + output.close(); + } catch (Exception e) { + LOGGER.error("Error in convert image", e); + } + } + /** + * 彩色转为黑白 + * @param src 源图像地址 + * @param output 目标图像地址 + */ + public final static void gray(BufferedImage src, OutputStream output, String type) { + try { + ColorSpace cs = ColorSpace.getInstance(ColorSpace.CS_GRAY); + ColorConvertOp op = new ColorConvertOp(cs, null); + src = op.filter(src, null); + // 输出为文件 + ImageIO.write(src, defaultString(type, DEFAULT_IMG_TYPE), output); + // 关闭流 + output.close(); + } catch (IOException e) { + LOGGER.error("Error in gray image", e); + } + } + /** + * 给图片添加文字水印 + * @param src 源图像 + * @param output 输出流 + * @param text 水印文字 + * @param font 水印的字体 + * @param color 水印的字体颜色 + * @param position 水印位置 {@link ImagePosition} + * @param x 修正值 + * @param y 修正值 + * @param alpha 透明度:alpha 必须是范围 [0.0, 1.0] 之内(包含边界值)的一个浮点数字 + */ + public final static void textStamp(BufferedImage src, OutputStream output, String type, String text, Font font, Color color + , int position, int x, int y, float alpha) { + try { + int width = src.getWidth(null); + int height = src.getHeight(null); + BufferedImage image = new BufferedImage(width, height, + BufferedImage.TYPE_INT_RGB); + Graphics2D g = image.createGraphics(); + g.drawImage(src, 0, 0, width, height, null); + g.setColor(color); + g.setFont(font); + g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, alpha)); + // 在指定坐标绘制水印文字 + ImagePosition boxPos = new ImagePosition(width, height, calcTextWidth(text) * font.getSize(), font.getSize(), position); + g.drawString(text, boxPos.getX(x), boxPos.getY(y)); + g.dispose(); + // 输出为文件 + ImageIO.write((BufferedImage) image, defaultString(type, DEFAULT_IMG_TYPE), output); + // 关闭流 + output.close(); + } catch (Exception e) { + LOGGER.error("Error in textStamp image", e); + } + } + + /** + * 给图片添加图片水印 + * @param src 源图像 + * @param output 输出流 + * @param stamp 水印图片 + * @param position 水印位置 {@link ImagePosition} + * @param x 修正值 + * @param y 修正值 + * @param alpha 透明度:alpha 必须是范围 [0.0, 1.0] 之内(包含边界值)的一个浮点数字 + */ + public final static void imageStamp(BufferedImage src, OutputStream output, String type, BufferedImage stamp + ,int position, int x, int y, float alpha) { + try { + int width = src.getWidth(); + int height = src.getHeight(); + BufferedImage image = new BufferedImage(width, height, + BufferedImage.TYPE_INT_RGB); + Graphics2D g = image.createGraphics(); + g.drawImage(src, 0, 0, width, height, null); + // 水印文件 + int stampWidth = stamp.getWidth(); + int stampHeight = stamp.getHeight(); + g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, alpha)); + ImagePosition boxPos = new ImagePosition(width, height, stampWidth, stampHeight, position); + g.drawImage(stamp, boxPos.getX(x), boxPos.getY(y), stampWidth, stampHeight, null); + // 水印文件结束 + g.dispose(); + // 输出为文件 + ImageIO.write((BufferedImage) image, defaultString(type, DEFAULT_IMG_TYPE), output); + // 关闭流 + output.close(); + } catch (Exception e) { + LOGGER.error("Error imageStamp",e); + } + } + /** + * 计算text的长度(一个中文算两个字符) + * @param text + * @return + */ + public final static int calcTextWidth(String text) { + int length = 0; + for (int i = 0; i < text.length(); i++) { + if (new String(text.charAt(i) + "").getBytes().length > 1) { + length += 2; + } else { + length += 1; + } + } + return length / 2; + } + + public static String defaultString(String str, String defaultStr) { + return ((str == null) ? defaultStr : str); + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/utils/Lazy.java b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/Lazy.java new file mode 100644 index 0000000..ce9b15e --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/Lazy.java @@ -0,0 +1,69 @@ +/** + * Copyright (c) 2018-2028, DreamLu 卢春梦 (qq596392912@gmail.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.tool.utils; + +import org.springframework.lang.Nullable; + +import java.io.Serializable; +import java.util.function.Supplier; + +/** + * Holder of a value that is computed lazy. + * + * @author L.cm + */ +public class Lazy implements Supplier, Serializable { + + @Nullable + private transient volatile Supplier supplier; + @Nullable + private T value; + + /** + * Creates new instance of Lazy. + * @param supplier Supplier + * @param 泛型标记 + * @return Lazy + */ + public static Lazy of(final Supplier supplier) { + return new Lazy<>(supplier); + } + + private Lazy(final Supplier supplier) { + this.supplier = supplier; + } + + /** + * Returns the value. Value will be computed on first call. + * @return lazy value + */ + @Nullable + @Override + public T get() { + return (supplier == null) ? value : computeValue(); + } + + @Nullable + private synchronized T computeValue() { + final Supplier s = supplier; + if (s != null) { + value = s.get(); + supplier = null; + } + return value; + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/utils/NumberUtil.java b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/NumberUtil.java new file mode 100644 index 0000000..4248fa1 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/NumberUtil.java @@ -0,0 +1,208 @@ +/** + * Copyright (c) 2018-2028, DreamLu 卢春梦 (qq596392912@gmail.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.tool.utils; + + +import org.springframework.lang.Nullable; + +/** + * 数字类型工具类 + * + * @author L.cm + */ +public class NumberUtil extends org.springframework.util.NumberUtils { + + //----------------------------------------------------------------------- + + /** + *

Convert a String to an int, returning + * zero if the conversion fails.

+ * + *

If the string is null, zero is returned.

+ * + *
+	 *   NumberUtil.toInt(null) = 0
+	 *   NumberUtil.toInt("")   = 0
+	 *   NumberUtil.toInt("1")  = 1
+	 * 
+ * + * @param str the string to convert, may be null + * @return the int represented by the string, or zero if + * conversion fails + */ + public static int toInt(final String str) { + return toInt(str, -1); + } + + /** + *

Convert a String to an int, returning a + * default value if the conversion fails.

+ * + *

If the string is null, the default value is returned.

+ * + *
+	 *   NumberUtil.toInt(null, 1) = 1
+	 *   NumberUtil.toInt("", 1)   = 1
+	 *   NumberUtil.toInt("1", 0)  = 1
+	 * 
+ * + * @param str the string to convert, may be null + * @param defaultValue the default value + * @return the int represented by the string, or the default if conversion fails + */ + public static int toInt(@Nullable final String str, final int defaultValue) { + if (str == null) { + return defaultValue; + } + try { + return Integer.valueOf(str); + } catch (final NumberFormatException nfe) { + return defaultValue; + } + } + + /** + *

Convert a String to a long, returning + * zero if the conversion fails.

+ * + *

If the string is null, zero is returned.

+ * + *
+	 *   NumberUtil.toLong(null) = 0L
+	 *   NumberUtil.toLong("")   = 0L
+	 *   NumberUtil.toLong("1")  = 1L
+	 * 
+ * + * @param str the string to convert, may be null + * @return the long represented by the string, or 0 if + * conversion fails + */ + public static long toLong(final String str) { + return toLong(str, 0L); + } + + /** + *

Convert a String to a long, returning a + * default value if the conversion fails.

+ * + *

If the string is null, the default value is returned.

+ * + *
+	 *   NumberUtil.toLong(null, 1L) = 1L
+	 *   NumberUtil.toLong("", 1L)   = 1L
+	 *   NumberUtil.toLong("1", 0L)  = 1L
+	 * 
+ * + * @param str the string to convert, may be null + * @param defaultValue the default value + * @return the long represented by the string, or the default if conversion fails + */ + public static long toLong(@Nullable final String str, final long defaultValue) { + if (str == null) { + return defaultValue; + } + try { + return Long.valueOf(str); + } catch (final NumberFormatException nfe) { + return defaultValue; + } + } + + /** + *

Convert a String to a Double + * + * @param value + * @return double value + */ + public static Double toDouble(String value) { + return toDouble(value, null); + } + + /** + *

Convert a String to a Double + * + * @param value + * @param defaultValue 默认值 + * @return double value + */ + public static Double toDouble(@Nullable String value, Double defaultValue) { + if (value != null) { + return Double.valueOf(value.trim()); + } + return defaultValue; + } + + /** + *

Convert a String to a Double + * + * @param value + * @return double value + */ + public static Float toFloat(String value) { + return toFloat(value, null); + } + + /** + *

Convert a String to a Double + * + * @param value + * @param defaultValue 默认值 + * @return double value + */ + public static Float toFloat(@Nullable String value, Float defaultValue) { + if (value != null) { + return Float.valueOf(value.trim()); + } + return defaultValue; + } + + /** + * All possible chars for representing a number as a String + */ + private final static char[] DIGITS = { + '0' , '1' , '2' , '3' , '4' , '5' , + '6' , '7' , '8' , '9' , 'a' , 'b' , + 'c' , 'd' , 'e' , 'f' , 'g' , 'h' , + 'i' , 'j' , 'k' , 'l' , 'm' , 'n' , + 'o' , 'p' , 'q' , 'r' , 's' , 't' , + 'u' , 'v' , 'w' , 'x' , 'y' , 'z' , + 'A' , 'B' , 'C' , 'D' , 'E' , 'F' , + 'G' , 'H' , 'I' , 'J' , 'K' , 'L' , + 'M' , 'N' , 'O' , 'P' , 'Q' , 'R' , + 'S' , 'T' , 'U' , 'V' , 'W' , 'X' , + 'Y' , 'Z' + }; + + /** + * 将 long 转短字符串 为 62 进制 + * @param i 数字 + * @return 短字符串 + */ + public static String to62String(long i) { + int radix = DIGITS.length; + char[] buf = new char[65]; + int charPos = 64; + i = -i; + while (i <= -radix) { + buf[charPos--] = DIGITS[(int)(-(i % radix))]; + i = i / radix; + } + buf[charPos] = DIGITS[(int)(-i)]; + + return new String(buf, charPos, (65 - charPos)); + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/utils/OKHttpUtil.java b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/OKHttpUtil.java new file mode 100644 index 0000000..42e3706 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/OKHttpUtil.java @@ -0,0 +1,191 @@ +/** + * Copyright (c) 2018-2028, DreamLu 卢春梦 (qq596392912@gmail.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.tool.utils; + +import lombok.extern.slf4j.Slf4j; +import okhttp3.*; + +import java.util.Map; + +/** + * Http请求工具类 + */ +@Slf4j +public class OKHttpUtil { + + public static MediaType JSON = MediaType.parse("application/json; charset=utf-8"); + public static MediaType XML = MediaType.parse("application/xml; charset=utf-8"); + + /** + * GET + * + * @param url 请求的url + * @param queries 请求的参数,在浏览器?后面的数据,没有可以传null + * @return + */ + public static String get(String url, Map queries) { + return get(url, null, queries); + } + + /** + * GET + * + * @param url 请求的url + * @param header 请求头 + * @param queries 请求的参数,在浏览器?后面的数据,没有可以传null + * @return + */ + public static String get(String url, Map header, Map queries) { + StringBuffer sb = new StringBuffer(url); + if (queries != null && queries.keySet().size() > 0) { + sb.append("?clientId=blade"); + queries.forEach((k, v) -> sb.append("&" + k + "=" + v)); + } + + Request.Builder builder = new Request.Builder(); + + //添加请求头 + if (header != null && header.keySet().size() > 0) { + header.forEach((k, v) -> builder.addHeader(k, v)); + } + + Request request = builder.url(sb.toString()).build(); + return getBody(request); + } + + /** + * POST + * + * @param url 请求的url + * @param params post form 提交的参数 + * @return + */ + public static String post(String url, Map params) { + return post(url, null, params); + } + + /** + * POST + * + * @param url 请求的url + * @param header 请求头 + * @param params post form 提交的参数 + * @return + */ + public static String post(String url, Map header, Map params) { + FormBody.Builder formBuilder = new FormBody.Builder().add("clientId", "blade"); + //添加参数 + if (params != null && params.keySet().size() > 0) { + params.forEach((k, v) -> formBuilder.add(k, v)); + } + + Request.Builder builder = new Request.Builder(); + //添加请求头 + if (header != null && header.keySet().size() > 0) { + header.forEach((k, v) -> builder.addHeader(k, v)); + } + + Request request = builder.url(url).post(formBuilder.build()).build(); + return getBody(request); + } + + /** + * POST请求发送JSON数据 + * @param url + * @param json + * @return + */ + public static String postJson(String url, String json) { + return postJson(url, null, json); + } + + /** + * POST请求发送JSON数据 + * @param url + * @param header + * @param json + * @return + */ + public static String postJson(String url, Map header, String json) { + return postContent(url, header, json, JSON); + } + + /** + * POST请求发送xml数据 + * @param url + * @param xml + * @return + */ + public static String postXml(String url, String xml) { + return postXml(url, null, xml); + } + + /** + * POST请求发送xml数据 + * @param url + * @param header + * @param xml + * @return + */ + public static String postXml(String url, Map header, String xml) { + return postContent(url, header, xml, XML); + } + + /** + * 发送POST请求 + * @param url + * @param header + * @param content + * @param mediaType + * @return + */ + public static String postContent(String url, Map header, String content, MediaType mediaType) { + RequestBody requestBody = RequestBody.create(mediaType, content); + Request.Builder builder = new Request.Builder(); + //添加请求头 + if (header != null && header.keySet().size() > 0) { + header.forEach((k, v) -> builder.addHeader(k, v)); + } + Request request = builder.url(url).post(requestBody).build(); + return getBody(request); + } + + /** + * 获取body + * + * @param request + * @return + */ + private static String getBody(Request request) { + String responseBody = ""; + Response response = null; + try { + OkHttpClient okHttpClient = new OkHttpClient(); + response = okHttpClient.newCall(request).execute(); + if (response.isSuccessful()) { + return response.body().string(); + } + } catch (Exception e) { + log.error("okhttp3 post error >> ex = {}", e.getMessage()); + } finally { + if (response != null) { + response.close(); + } + } + return responseBody; + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/utils/ObjectUtil.java b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/ObjectUtil.java new file mode 100644 index 0000000..d003164 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/ObjectUtil.java @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2018-2028, DreamLu 卢春梦 (qq596392912@gmail.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.tool.utils; + +import org.springframework.lang.Nullable; + +/** + * 对象工具类 + * + * @author L.cm + */ +public class ObjectUtil extends org.springframework.util.ObjectUtils { + + /** + * 判断元素不为空 + */ + public static boolean isNotEmpty(@Nullable Object obj) { + return !ObjectUtil.isEmpty(obj); + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/utils/PathUtil.java b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/PathUtil.java new file mode 100644 index 0000000..bcd18d2 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/PathUtil.java @@ -0,0 +1,70 @@ +/** + * Copyright (c) 2018-2028, DreamLu 卢春梦 (qq596392912@gmail.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.tool.utils; + +import org.springframework.lang.Nullable; + +import java.io.File; +import java.net.URL; + +/** + * 用来获取各种目录 + * + * @author L.cm + */ +public class PathUtil { + public static final String FILE_PROTOCOL = "file"; + public static final String JAR_PROTOCOL = "jar"; + public static final String ZIP_PROTOCOL = "zip"; + public static final String FILE_PROTOCOL_PREFIX = "file:"; + public static final String JAR_FILE_SEPARATOR = "!/"; + + /** + * 获取jar包运行时的当前目录 + * @return {String} + */ + @Nullable + public static String getJarPath() { + try { + URL url = PathUtil.class.getResource("/").toURI().toURL(); + return PathUtil.toFilePath(url); + } catch (Exception e) { + String path = PathUtil.class.getResource("").getPath(); + return new File(path).getParentFile().getParentFile().getAbsolutePath(); + } + } + + @Nullable + public static String toFilePath(@Nullable URL url) { + if (url == null) { return null; } + String protocol = url.getProtocol(); + String file = URLUtil.decodeURL(url.getPath(), Charsets.UTF_8); + if (FILE_PROTOCOL.equals(protocol)) { + return new File(file).getParentFile().getParentFile().getAbsolutePath(); + } else if (JAR_PROTOCOL.equals(protocol) || ZIP_PROTOCOL.equals(protocol)) { + int ipos = file.indexOf(JAR_FILE_SEPARATOR); + if (ipos > 0) { + file = file.substring(0, ipos); + } + if (file.startsWith(FILE_PROTOCOL_PREFIX)) { + file = file.substring(FILE_PROTOCOL_PREFIX.length()); + } + return new File(file).getParentFile().getAbsolutePath(); + } + return file; + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/utils/ProtostuffUtil.java b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/ProtostuffUtil.java new file mode 100644 index 0000000..d4a600c --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/ProtostuffUtil.java @@ -0,0 +1,90 @@ +/** + * Copyright (c) 2018-2028, DreamLu 卢春梦 (qq596392912@gmail.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.tool.utils; + +import io.protostuff.LinkedBuffer; +import io.protostuff.ProtostuffIOUtil; +import io.protostuff.Schema; +import io.protostuff.runtime.RuntimeSchema; + +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Protostuff 工具类 + */ +public class ProtostuffUtil { + + /** + * 避免每次序列化都重新申请Buffer空间 + */ + private static LinkedBuffer buffer = LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE); + /** + * 缓存Schema + */ + private static Map, Schema> schemaCache = new ConcurrentHashMap<>(); + + /** + * 序列化方法,把指定对象序列化成字节数组 + * + * @param obj + * @param + * @return + */ + @SuppressWarnings("unchecked") + public static byte[] serialize(T obj) { + Class clazz = (Class) obj.getClass(); + Schema schema = getSchema(clazz); + byte[] data; + try { + data = ProtostuffIOUtil.toByteArray(obj, schema, buffer); + } finally { + buffer.clear(); + } + return data; + } + + /** + * 反序列化方法,将字节数组反序列化成指定Class类型 + * + * @param data + * @param clazz + * @param + * @return + */ + public static T deserialize(byte[] data, Class clazz) { + Schema schema = getSchema(clazz); + T obj = schema.newMessage(); + ProtostuffIOUtil.mergeFrom(data, obj, schema); + return obj; + } + + @SuppressWarnings("unchecked") + private static Schema getSchema(Class clazz) { + Schema schema = (Schema) schemaCache.get(clazz); + if (Objects.isNull(schema)) { + //这个schema通过RuntimeSchema进行懒创建并缓存 + //所以可以一直调用RuntimeSchema.getSchema(),这个方法是线程安全的 + schema = RuntimeSchema.getSchema(clazz); + if (Objects.nonNull(schema)) { + schemaCache.put(clazz, schema); + } + } + return schema; + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/utils/RandomType.java b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/RandomType.java new file mode 100644 index 0000000..9a15366 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/RandomType.java @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2018-2028, DreamLu 卢春梦 (qq596392912@gmail.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.tool.utils; + +/** + * 生成的随机数类型 + * + * @author L.cm + */ +public enum RandomType { + /** + * INT STRING ALL + */ + INT, STRING, ALL; +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/utils/RegexUtil.java b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/RegexUtil.java new file mode 100644 index 0000000..762ebdd --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/RegexUtil.java @@ -0,0 +1,112 @@ +/** + * Copyright (c) 2018-2028, DreamLu 卢春梦 (qq596392912@gmail.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.tool.utils; + +import org.springframework.lang.Nullable; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * 正则表达式工具 + * + * @author L.cm + */ +public class RegexUtil { + /** + * 用户名 + */ + public static final String USER_NAME = "^[a-zA-Z\\u4E00-\\u9FA5][a-zA-Z0-9_\\u4E00-\\u9FA5]{1,11}$"; + + /** + * 密码 + */ + public static final String USER_PASSWORD = "^.{6,32}$"; + + /** + * 邮箱 + */ + public static final String EMAIL = "^\\w+([-+.]*\\w+)*@([\\da-z](-[\\da-z])?)+(\\.{1,2}[a-z]+)+$"; + + /** + * 手机号 + */ + public static final String PHONE = "^1[3456789]\\d{9}$"; + + /** + * 手机号或者邮箱 + */ + public static final String EMAIL_OR_PHONE = EMAIL + "|" + PHONE; + + /** + * URL路径 + */ + public static final String URL = "^(https?:\\/\\/)?([\\da-z\\.-]+)\\.([a-z\\.]{2,6})(:[\\d]+)?([\\/\\w\\.-]*)*\\/?$"; + + /** + * 身份证校验,初级校验,具体规则有一套算法 + */ + public static final String ID_CARD = "^\\d{15}$|^\\d{17}([0-9]|X)$"; + + /** + * 域名校验 + */ + public static final String DOMAIN = "^[0-9a-zA-Z]+[0-9a-zA-Z\\.-]*\\.[a-zA-Z]{2,4}$"; + + /** + * + * 编译传入正则表达式和字符串去匹配,忽略大小写 + * @param regex 正则 + * @param beTestString 字符串 + * @return {boolean} + */ + public static boolean match(String regex, String beTestString) { + Pattern pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE); + Matcher matcher = pattern.matcher(beTestString); + return matcher.matches(); + } + + /** + * + * 编译传入正则表达式在字符串中寻找,如果匹配到则为true + * @param regex 正则 + * @param beTestString 字符串 + * @return {boolean} + */ + public static boolean find(String regex, String beTestString) { + Pattern pattern = Pattern.compile(regex); + Matcher matcher = pattern.matcher(beTestString); + return matcher.find(); + } + + /** + * 编译传入正则表达式在字符串中寻找,如果找到返回第一个结果 + * 找不到返回null + * @param regex 正则 + * @param beFoundString 字符串 + * @return {boolean} + */ + @Nullable + public static String findResult(String regex, String beFoundString) { + Pattern pattern = Pattern.compile(regex); + Matcher matcher = pattern.matcher(beFoundString); + if (matcher.find()) { + return matcher.group(); + } + return null; + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/utils/ResourceUtil.java b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/ResourceUtil.java new file mode 100644 index 0000000..937eff9 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/ResourceUtil.java @@ -0,0 +1,69 @@ +/** + * Copyright (c) 2018-2028, DreamLu 卢春梦 (qq596392912@gmail.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.tool.utils; + +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.Resource; +import org.springframework.core.io.UrlResource; +import org.springframework.core.io.support.ResourcePatternResolver; +import org.springframework.util.Assert; + +import java.io.IOException; + +/** + * 资源工具类 + * + * @author L.cm + */ +public class ResourceUtil extends org.springframework.util.ResourceUtils { + public static final String HTTP_REGEX = "^https?:.+$"; + public static final String FTP_URL_PREFIX = "ftp:"; + + /** + * 获取资源 + * + * 支持一下协议: + *

+ * 1. classpath: + * 2. file: + * 3. ftp: + * 4. http: and https: + * 5. classpath*: + * 6. C:/dir1/ and /Users/lcm + *

+ * @param resourceLocation 资源路径 + * @throws IOException IOException + * @return {Resource} + */ + public static Resource getResource(String resourceLocation) throws IOException { + Assert.notNull(resourceLocation, "Resource location must not be null"); + if (resourceLocation.startsWith(CLASSPATH_URL_PREFIX)) { + return new ClassPathResource(resourceLocation); + } + if (resourceLocation.startsWith(FTP_URL_PREFIX)) { + return new UrlResource(resourceLocation); + } + if (resourceLocation.matches(HTTP_REGEX)) { + return new UrlResource(resourceLocation); + } + if (resourceLocation.startsWith(ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX)) { + return SpringUtil.getContext().getResource(resourceLocation); + } + return new FileSystemResource(resourceLocation); + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/utils/SpringUtil.java b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/SpringUtil.java new file mode 100644 index 0000000..a677189 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/SpringUtil.java @@ -0,0 +1,67 @@ +/** + * Copyright (c) 2018-2028, DreamLu 卢春梦 (qq596392912@gmail.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.tool.utils; + +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.ApplicationEvent; + +/** + * spring 工具类 + */ +public class SpringUtil implements ApplicationContextAware { + + private static ApplicationContext context; + + @Override + public void setApplicationContext(ApplicationContext context) throws BeansException { + SpringUtil.context = context; + } + + public static T getBean(Class clazz) { + if (clazz == null) { + return null; + } + return context.getBean(clazz); + } + + public static T getBean(String beanId) { + if (beanId == null) { + return null; + } + return (T) context.getBean(beanId); + } + + public static T getBean(String beanName, Class clazz) { + if (null == beanName || "".equals(beanName.trim())) { + return null; + } + if (clazz == null) return null; + return (T) context.getBean(beanName, clazz); + } + + public static ApplicationContext getContext() { + if (context == null) return null; + return context; + } + + public static void publishEvent(ApplicationEvent event) { + if (context == null) return; + context.publishEvent(event); + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/utils/StringPool.java b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/StringPool.java new file mode 100644 index 0000000..59a568d --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/StringPool.java @@ -0,0 +1,81 @@ +/** + * Copyright (c) 2018-2028, DreamLu 卢春梦 (qq596392912@gmail.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.tool.utils; + +/** + * 静态 String 池 + * + * @author L.cm + */ +public interface StringPool { + + String AMPERSAND = "&"; + String AND = "and"; + String AT = "@"; + String ASTERISK = "*"; + String STAR = ASTERISK; + char SLASH = '/'; + char BACK_SLASH = '\\'; + String COLON = ":"; + String COMMA = ","; + String DASH = "-"; + String DOLLAR = "$"; + String DOT = "."; + String EMPTY = ""; + String EMPTY_JSON = "{}"; + String EQUALS = "="; + String FALSE = "false"; + String HASH = "#"; + String HAT = "^"; + String LEFT_BRACE = "{"; + String LEFT_BRACKET = "("; + String LEFT_CHEV = "<"; + String NEWLINE = "\n"; + String N = "n"; + String NO = "no"; + String NULL = "null"; + String OFF = "off"; + String ON = "on"; + String PERCENT = "%"; + String PIPE = "|"; + String PLUS = "+"; + String QUESTION_MARK = "?"; + String EXCLAMATION_MARK = "!"; + String QUOTE = "\""; + String RETURN = "\r"; + String TAB = "\t"; + String RIGHT_BRACE = "}"; + String RIGHT_BRACKET = ")"; + String RIGHT_CHEV = ">"; + String SEMICOLON = ";"; + String SINGLE_QUOTE = "'"; + String BACKTICK = "`"; + String SPACE = " "; + String TILDA = "~"; + String LEFT_SQ_BRACKET = "["; + String RIGHT_SQ_BRACKET = "]"; + String TRUE = "true"; + String UNDERSCORE = "_"; + String UTF_8 = "UTF-8"; + String GBK = "GBK"; + String ISO_8859_1 = "ISO-8859-1"; + String Y = "y"; + String YES = "yes"; + String ONE = "1"; + String ZERO = "0"; + String DOLLAR_LEFT_BRACE= "${"; + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/utils/StringUtil.java b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/StringUtil.java new file mode 100644 index 0000000..9eeb4e2 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/StringUtil.java @@ -0,0 +1,1336 @@ +/** + * Copyright (c) 2018-2028, DreamLu 卢春梦 (qq596392912@gmail.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.tool.utils; + +import org.springblade.core.tool.support.StrFormatter; +import org.springblade.core.tool.support.StrSpliter; +import org.springframework.util.Assert; +import org.springframework.web.util.HtmlUtils; + +import java.io.StringReader; +import java.io.StringWriter; +import java.text.MessageFormat; +import java.util.*; +import java.util.concurrent.ThreadLocalRandom; +import java.util.stream.Stream; + +/** + * 继承自Spring util的工具类,减少jar依赖 + */ +public class StringUtil extends org.springframework.util.StringUtils { + + public static final int INDEX_NOT_FOUND = -1; + /** + * Check whether the given {@code CharSequence} contains actual text. + *

More specifically, this method returns {@code true} if the + * {@code CharSequence} is not {@code null}, its length is greater than + * 0, and it contains at least one non-whitespace character. + *

+     * StringUtil.isBlank(null) = true
+     * StringUtil.isBlank("") = true
+     * StringUtil.isBlank(" ") = true
+     * StringUtil.isBlank("12345") = false
+     * StringUtil.isBlank(" 12345 ") = false
+     * 
+ * + * @param cs the {@code CharSequence} to check (may be {@code null}) + * @return {@code true} if the {@code CharSequence} is not {@code null}, + * its length is greater than 0, and it does not contain whitespace only + * @see Character#isWhitespace + */ + public static boolean isBlank(final CharSequence cs) { + return !StringUtil.hasText(cs); + } + + /** + *

Checks if a CharSequence is not empty (""), not null and not whitespace only.

+ *
+     * StringUtil.isNotBlank(null)	  = false
+     * StringUtil.isNotBlank("")		= false
+     * StringUtil.isNotBlank(" ")	   = false
+     * StringUtil.isNotBlank("bob")	 = true
+     * StringUtil.isNotBlank("  bob  ") = true
+     * 
+ * + * @param cs the CharSequence to check, may be null + * @return {@code true} if the CharSequence is + * not empty and not null and not whitespace + * @see Character#isWhitespace + */ + public static boolean isNotBlank(final CharSequence cs) { + return StringUtil.hasText(cs); + } + + /** + * 有 任意 一个 Blank + * @param css CharSequence + * @return boolean + */ + public static boolean isAnyBlank(final CharSequence... css) { + if (ObjectUtil.isEmpty(css)) { + return true; + } + return Stream.of(css).anyMatch(StringUtil::isBlank); + } + + /** + * 是否全非 Blank + * @param css CharSequence + * @return boolean + */ + public static boolean isNoneBlank(final CharSequence... css) { + if (ObjectUtil.isEmpty(css)) { + return false; + } + return Stream.of(css).allMatch(StringUtil::isNotBlank); + } + + /** + * 判断一个字符串是否是数字 + * + * @param cs the CharSequence to check, may be null + * @return {boolean} + */ + public static boolean isNumeric(final CharSequence cs) { + if (isBlank(cs)) { + return false; + } + for (int i = cs.length(); --i >= 0; ) { + int chr = cs.charAt(i); + if (chr < 48 || chr > 57) + return false; + } + return true; + } + + /** + * Convert a {@code Collection} into a delimited {@code String} (e.g., CSV). + *

Useful for {@code toString()} implementations. + * + * @param coll the {@code Collection} to convert + * @return the delimited {@code String} + */ + public static String join(Collection coll) { + return StringUtil.collectionToCommaDelimitedString(coll); + } + + /** + * Convert a {@code Collection} into a delimited {@code String} (e.g. CSV). + *

Useful for {@code toString()} implementations. + * + * @param coll the {@code Collection} to convert + * @param delim the delimiter to use (typically a ",") + * @return the delimited {@code String} + */ + public static String join(Collection coll, String delim) { + return StringUtil.collectionToDelimitedString(coll, delim); + } + + /** + * Convert a {@code String} array into a comma delimited {@code String} + * (i.e., CSV). + *

Useful for {@code toString()} implementations. + * + * @param arr the array to display + * @return the delimited {@code String} + */ + public static String join(Object[] arr) { + return StringUtil.arrayToCommaDelimitedString(arr); + } + + /** + * Convert a {@code String} array into a delimited {@code String} (e.g. CSV). + *

Useful for {@code toString()} implementations. + * + * @param arr the array to display + * @param delim the delimiter to use (typically a ",") + * @return the delimited {@code String} + */ + public static String join(Object[] arr, String delim) { + return StringUtil.arrayToDelimitedString(arr, delim); + } + + /** + * 生成uuid + * + * @return UUID + */ + public static String randomUUID() { + ThreadLocalRandom random = ThreadLocalRandom.current(); + return new UUID(random.nextLong(), random.nextLong()).toString().replace(StringPool.DASH, StringPool.EMPTY); + } + + /** + * 转义HTML用于安全过滤 + * + * @param html html + * @return {String} + */ + public static String escapeHtml(String html) { + return HtmlUtils.htmlEscape(html); + } + + /** + * 清理字符串,清理出某些不可见字符 + * + * @param txt 字符串 + * @return {String} + */ + public static String cleanChars(String txt) { + return txt.replaceAll("[  `·•�\\f\\t\\v\\s]", ""); + } + + // 随机字符串 + private static final String _INT = "0123456789"; + private static final String _STR = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + private static final String _ALL = _INT + _STR; + + /** + * 随机数生成 + * + * @param count 字符长度 + * @return 随机数 + */ + public static String random(int count) { + return StringUtil.random(count, RandomType.ALL); + } + + /** + * 随机数生成 + * + * @param count 字符长度 + * @param randomType 随机数类别 + * @return 随机数 + */ + public static String random(int count, RandomType randomType) { + if (count == 0) { + return ""; + } + Assert.isTrue(count > 0, "Requested random string length " + count + " is less than 0."); + final ThreadLocalRandom random = ThreadLocalRandom.current(); + char[] buffer = new char[count]; + for (int i = 0; i < count; i++) { + if (RandomType.INT == randomType) { + buffer[i] = _INT.charAt(random.nextInt(_INT.length())); + } else if (RandomType.STRING == randomType) { + buffer[i] = _STR.charAt(random.nextInt(_STR.length())); + } else { + buffer[i] = _ALL.charAt(random.nextInt(_ALL.length())); + } + } + return new String(buffer); + } + + /** + * 格式化文本, {} 表示占位符
+ * 此方法只是简单将占位符 {} 按照顺序替换为参数
+ * 如果想输出 {} 使用 \\转义 { 即可,如果想输出 {} 之前的 \ 使用双转义符 \\\\ 即可
+ * 例:
+ * 通常使用:format("this is {} for {}", "a", "b") =》 this is a for b
+ * 转义{}: format("this is \\{} for {}", "a", "b") =》 this is \{} for a
+ * 转义\: format("this is \\\\{} for {}", "a", "b") =》 this is \a for b
+ * + * @param template 文本模板,被替换的部分用 {} 表示 + * @param params 参数值 + * @return 格式化后的文本 + */ + public static String format(CharSequence template, Object... params) { + if (null == template) { + return null; + } + if (Func.isEmpty(params) || isBlank(template)) { + return template.toString(); + } + return StrFormatter.format(template.toString(), params); + } + + /** + * 有序的格式化文本,使用{number}做为占位符
+ * 例:
+ * 通常使用:format("this is {0} for {1}", "a", "b") =》 this is a for b
+ * + * @param pattern 文本格式 + * @param arguments 参数 + * @return 格式化后的文本 + */ + public static String indexedFormat(CharSequence pattern, Object... arguments) { + return MessageFormat.format(pattern.toString(), arguments); + } + + /** + * 格式化文本,使用 {varName} 占位
+ * map = {a: "aValue", b: "bValue"} format("{a} and {b}", map) ---=》 aValue and bValue + * + * @param template 文本模板,被替换的部分用 {key} 表示 + * @param map 参数值对 + * @return 格式化后的文本 + */ + public static String format(CharSequence template, Map map) { + if (null == template) { + return null; + } + if (null == map || map.isEmpty()) { + return template.toString(); + } + + String template2 = template.toString(); + for (Map.Entry entry : map.entrySet()) { + template2 = template2.replace("{" + entry.getKey() + "}", Func.toStr(entry.getValue())); + } + return template2; + } + + /** + * 切分字符串,不去除切分后每个元素两边的空白符,不去除空白项 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符 + * @param limit 限制分片数,-1不限制 + * @return 切分后的集合 + */ + public static List split(CharSequence str, char separator, int limit) { + return split(str, separator, limit, false, false); + } + + /** + * 切分字符串,去除切分后每个元素两边的空白符,去除空白项 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符 + * @return 切分后的集合 + * @since 3.1.2 + */ + public static List splitTrim(CharSequence str, char separator) { + return splitTrim(str, separator, -1); + } + + /** + * 切分字符串,去除切分后每个元素两边的空白符,去除空白项 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符 + * @return 切分后的集合 + * @since 3.2.0 + */ + public static List splitTrim(CharSequence str, CharSequence separator) { + return splitTrim(str, separator, -1); + } + + /** + * 切分字符串,去除切分后每个元素两边的空白符,去除空白项 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符 + * @param limit 限制分片数,-1不限制 + * @return 切分后的集合 + * @since 3.1.0 + */ + public static List splitTrim(CharSequence str, char separator, int limit) { + return split(str, separator, limit, true, true); + } + + /** + * 切分字符串,去除切分后每个元素两边的空白符,去除空白项 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符 + * @param limit 限制分片数,-1不限制 + * @return 切分后的集合 + * @since 3.2.0 + */ + public static List splitTrim(CharSequence str, CharSequence separator, int limit) { + return split(str, separator, limit, true, true); + } + + /** + * 切分字符串,不限制分片数量 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符 + * @param isTrim 是否去除切分字符串后每个元素两边的空格 + * @param ignoreEmpty 是否忽略空串 + * @return 切分后的集合 + * @since 3.0.8 + */ + public static List split(CharSequence str, char separator, boolean isTrim, boolean ignoreEmpty) { + return split(str, separator, 0, isTrim, ignoreEmpty); + } + + /** + * 切分字符串 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符 + * @param limit 限制分片数,-1不限制 + * @param isTrim 是否去除切分字符串后每个元素两边的空格 + * @param ignoreEmpty 是否忽略空串 + * @return 切分后的集合 + * @since 3.0.8 + */ + public static List split(CharSequence str, char separator, int limit, boolean isTrim, boolean ignoreEmpty) { + if (null == str) { + return new ArrayList<>(0); + } + return StrSpliter.split(str.toString(), separator, limit, isTrim, ignoreEmpty); + } + + /** + * 切分字符串 + * + * @param str 被切分的字符串 + * @param separator 分隔符字符 + * @param limit 限制分片数,-1不限制 + * @param isTrim 是否去除切分字符串后每个元素两边的空格 + * @param ignoreEmpty 是否忽略空串 + * @return 切分后的集合 + * @since 3.2.0 + */ + public static List split(CharSequence str, CharSequence separator, int limit, boolean isTrim, boolean ignoreEmpty) { + if (null == str) { + return new ArrayList<>(0); + } + final String separatorStr = (null == separator) ? null : separator.toString(); + return StrSpliter.split(str.toString(), separatorStr, limit, isTrim, ignoreEmpty); + } + + /** + * 切分字符串 + * + * @param str 被切分的字符串 + * @param separator 分隔符 + * @return 字符串 + */ + public static String[] split(CharSequence str, CharSequence separator) { + if (str == null) { + return new String[]{}; + } + + final String separatorStr = (null == separator) ? null : separator.toString(); + return StrSpliter.splitToArray(str.toString(), separatorStr, 0, false, false); + } + + /** + * 根据给定长度,将给定字符串截取为多个部分 + * + * @param str 字符串 + * @param len 每一个小节的长度 + * @return 截取后的字符串数组 + * @see StrSpliter#splitByLength(String, int) + */ + public static String[] split(CharSequence str, int len) { + if (null == str) { + return new String[]{}; + } + return StrSpliter.splitByLength(str.toString(), len); + } + + /** + * 指定字符是否在字符串中出现过 + * + * @param str 字符串 + * @param searchChar 被查找的字符 + * @return 是否包含 + * @since 3.1.2 + */ + public static boolean contains(CharSequence str, char searchChar) { + return indexOf(str, searchChar) > -1; + } + + /** + * 查找指定字符串是否包含指定字符串列表中的任意一个字符串 + * + * @param str 指定字符串 + * @param testStrs 需要检查的字符串数组 + * @return 是否包含任意一个字符串 + * @since 3.2.0 + */ + public static boolean containsAny(CharSequence str, CharSequence... testStrs) { + return null != getContainsStr(str, testStrs); + } + + /** + * 查找指定字符串是否包含指定字符串列表中的任意一个字符串,如果包含返回找到的第一个字符串 + * + * @param str 指定字符串 + * @param testStrs 需要检查的字符串数组 + * @return 被包含的第一个字符串 + * @since 3.2.0 + */ + public static String getContainsStr(CharSequence str, CharSequence... testStrs) { + if (isEmpty(str) || Func.isEmpty(testStrs)) { + return null; + } + for (CharSequence checkStr : testStrs) { + if (str.toString().contains(checkStr)) { + return checkStr.toString(); + } + } + return null; + } + + /** + * 是否包含特定字符,忽略大小写,如果给定两个参数都为null,返回true + * + * @param str 被检测字符串 + * @param testStr 被测试是否包含的字符串 + * @return 是否包含 + */ + public static boolean containsIgnoreCase(CharSequence str, CharSequence testStr) { + if (null == str) { + // 如果被监测字符串和 + return null == testStr; + } + return str.toString().toLowerCase().contains(testStr.toString().toLowerCase()); + } + + /** + * 查找指定字符串是否包含指定字符串列表中的任意一个字符串
+ * 忽略大小写 + * + * @param str 指定字符串 + * @param testStrs 需要检查的字符串数组 + * @return 是否包含任意一个字符串 + * @since 3.2.0 + */ + public static boolean containsAnyIgnoreCase(CharSequence str, CharSequence... testStrs) { + return null != getContainsStrIgnoreCase(str, testStrs); + } + + /** + * 查找指定字符串是否包含指定字符串列表中的任意一个字符串,如果包含返回找到的第一个字符串
+ * 忽略大小写 + * + * @param str 指定字符串 + * @param testStrs 需要检查的字符串数组 + * @return 被包含的第一个字符串 + * @since 3.2.0 + */ + public static String getContainsStrIgnoreCase(CharSequence str, CharSequence... testStrs) { + if (isEmpty(str) || Func.isEmpty(testStrs)) { + return null; + } + for (CharSequence testStr : testStrs) { + if (containsIgnoreCase(str, testStr)) { + return testStr.toString(); + } + } + return null; + } + + /** + * 改进JDK subString
+ * index从0开始计算,最后一个字符为-1
+ * 如果from和to位置一样,返回 ""
+ * 如果from或to为负数,则按照length从后向前数位置,如果绝对值大于字符串长度,则from归到0,to归到length
+ * 如果经过修正的index中from大于to,则互换from和to example:
+ * abcdefgh 2 3 =》 c
+ * abcdefgh 2 -3 =》 cde
+ * + * @param str String + * @param fromIndex 开始的index(包括) + * @param toIndex 结束的index(不包括) + * @return 字串 + */ + public static String sub(CharSequence str, int fromIndex, int toIndex) { + if (isEmpty(str)) { + return StringPool.EMPTY; + } + int len = str.length(); + + if (fromIndex < 0) { + fromIndex = len + fromIndex; + if (fromIndex < 0) { + fromIndex = 0; + } + } else if (fromIndex > len) { + fromIndex = len; + } + + if (toIndex < 0) { + toIndex = len + toIndex; + if (toIndex < 0) { + toIndex = len; + } + } else if (toIndex > len) { + toIndex = len; + } + + if (toIndex < fromIndex) { + int tmp = fromIndex; + fromIndex = toIndex; + toIndex = tmp; + } + + if (fromIndex == toIndex) { + return StringPool.EMPTY; + } + + return str.toString().substring(fromIndex, toIndex); + } + + + + /** + * 截取分隔字符串之前的字符串,不包括分隔字符串
+ * 如果给定的字符串为空串(null或"")或者分隔字符串为null,返回原字符串
+ * 如果分隔字符串为空串"",则返回空串,如果分隔字符串未找到,返回原字符串 + * + * 栗子: + * + *

+     * StringUtil.subBefore(null, *)      = null
+     * StringUtil.subBefore("", *)        = ""
+     * StringUtil.subBefore("abc", "a")   = ""
+     * StringUtil.subBefore("abcba", "b") = "a"
+     * StringUtil.subBefore("abc", "c")   = "ab"
+     * StringUtil.subBefore("abc", "d")   = "abc"
+     * StringUtil.subBefore("abc", "")    = ""
+     * StringUtil.subBefore("abc", null)  = "abc"
+     * 
+ * + * @param string 被查找的字符串 + * @param separator 分隔字符串(不包括) + * @param isLastSeparator 是否查找最后一个分隔字符串(多次出现分隔字符串时选取最后一个),true为选取最后一个 + * @return 切割后的字符串 + * @since 3.1.1 + */ + public static String subBefore(CharSequence string, CharSequence separator, boolean isLastSeparator) { + if (isEmpty(string) || separator == null) { + return null == string ? null : string.toString(); + } + + final String str = string.toString(); + final String sep = separator.toString(); + if (sep.isEmpty()) { + return StringPool.EMPTY; + } + final int pos = isLastSeparator ? str.lastIndexOf(sep) : str.indexOf(sep); + if (pos == INDEX_NOT_FOUND) { + return str; + } + return str.substring(0, pos); + } + + /** + * 截取分隔字符串之后的字符串,不包括分隔字符串
+ * 如果给定的字符串为空串(null或""),返回原字符串
+ * 如果分隔字符串为空串(null或""),则返回空串,如果分隔字符串未找到,返回空串 + * + * 栗子: + * + *
+     * StringUtil.subAfter(null, *)      = null
+     * StringUtil.subAfter("", *)        = ""
+     * StringUtil.subAfter(*, null)      = ""
+     * StringUtil.subAfter("abc", "a")   = "bc"
+     * StringUtil.subAfter("abcba", "b") = "cba"
+     * StringUtil.subAfter("abc", "c")   = ""
+     * StringUtil.subAfter("abc", "d")   = ""
+     * StringUtil.subAfter("abc", "")    = "abc"
+     * 
+ * + * @param string 被查找的字符串 + * @param separator 分隔字符串(不包括) + * @param isLastSeparator 是否查找最后一个分隔字符串(多次出现分隔字符串时选取最后一个),true为选取最后一个 + * @return 切割后的字符串 + * @since 3.1.1 + */ + public static String subAfter(CharSequence string, CharSequence separator, boolean isLastSeparator) { + if (isEmpty(string)) { + return null == string ? null : string.toString(); + } + if (separator == null) { + return StringPool.EMPTY; + } + final String str = string.toString(); + final String sep = separator.toString(); + final int pos = isLastSeparator ? str.lastIndexOf(sep) : str.indexOf(sep); + if (pos == INDEX_NOT_FOUND) { + return StringPool.EMPTY; + } + return str.substring(pos + separator.length()); + } + + /** + * 截取指定字符串中间部分,不包括标识字符串
+ * + * 栗子: + * + *
+     * StringUtil.subBetween("wx[b]yz", "[", "]") = "b"
+     * StringUtil.subBetween(null, *, *)          = null
+     * StringUtil.subBetween(*, null, *)          = null
+     * StringUtil.subBetween(*, *, null)          = null
+     * StringUtil.subBetween("", "", "")          = ""
+     * StringUtil.subBetween("", "", "]")         = null
+     * StringUtil.subBetween("", "[", "]")        = null
+     * StringUtil.subBetween("yabcz", "", "")     = ""
+     * StringUtil.subBetween("yabcz", "y", "z")   = "abc"
+     * StringUtil.subBetween("yabczyabcz", "y", "z")   = "abc"
+     * 
+ * + * @param str 被切割的字符串 + * @param before 截取开始的字符串标识 + * @param after 截取到的字符串标识 + * @return 截取后的字符串 + * @since 3.1.1 + */ + public static String subBetween(CharSequence str, CharSequence before, CharSequence after) { + if (str == null || before == null || after == null) { + return null; + } + + final String str2 = str.toString(); + final String before2 = before.toString(); + final String after2 = after.toString(); + + final int start = str2.indexOf(before2); + if (start != INDEX_NOT_FOUND) { + final int end = str2.indexOf(after2, start + before2.length()); + if (end != INDEX_NOT_FOUND) { + return str2.substring(start + before2.length(), end); + } + } + return null; + } + + /** + * 截取指定字符串中间部分,不包括标识字符串
+ * + * 栗子: + * + *
+     * StringUtil.subBetween(null, *)            = null
+     * StringUtil.subBetween("", "")             = ""
+     * StringUtil.subBetween("", "tag")          = null
+     * StringUtil.subBetween("tagabctag", null)  = null
+     * StringUtil.subBetween("tagabctag", "")    = ""
+     * StringUtil.subBetween("tagabctag", "tag") = "abc"
+     * 
+ * + * @param str 被切割的字符串 + * @param beforeAndAfter 截取开始和结束的字符串标识 + * @return 截取后的字符串 + * @since 3.1.1 + */ + public static String subBetween(CharSequence str, CharSequence beforeAndAfter) { + return subBetween(str, beforeAndAfter, beforeAndAfter); + } + + /** + * 去掉指定前缀 + * + * @param str 字符串 + * @param prefix 前缀 + * @return 切掉后的字符串,若前缀不是 preffix, 返回原字符串 + */ + public static String removePrefix(CharSequence str, CharSequence prefix) { + if (isEmpty(str) || isEmpty(prefix)) { + return StringPool.EMPTY; + } + + final String str2 = str.toString(); + if (str2.startsWith(prefix.toString())) { + return subSuf(str2, prefix.length());// 截取后半段 + } + return str2; + } + + /** + * 忽略大小写去掉指定前缀 + * + * @param str 字符串 + * @param prefix 前缀 + * @return 切掉后的字符串,若前缀不是 prefix, 返回原字符串 + */ + public static String removePrefixIgnoreCase(CharSequence str, CharSequence prefix) { + if (isEmpty(str) || isEmpty(prefix)) { + return StringPool.EMPTY; + } + + final String str2 = str.toString(); + if (str2.toLowerCase().startsWith(prefix.toString().toLowerCase())) { + return subSuf(str2, prefix.length());// 截取后半段 + } + return str2; + } + + /** + * 去掉指定后缀 + * + * @param str 字符串 + * @param suffix 后缀 + * @return 切掉后的字符串,若后缀不是 suffix, 返回原字符串 + */ + public static String removeSuffix(CharSequence str, CharSequence suffix) { + if (isEmpty(str) || isEmpty(suffix)) { + return StringPool.EMPTY; + } + + final String str2 = str.toString(); + if (str2.endsWith(suffix.toString())) { + return subPre(str2, str2.length() - suffix.length());// 截取前半段 + } + return str2; + } + + /** + * 去掉指定后缀,并小写首字母 + * + * @param str 字符串 + * @param suffix 后缀 + * @return 切掉后的字符串,若后缀不是 suffix, 返回原字符串 + */ + public static String removeSufAndLowerFirst(CharSequence str, CharSequence suffix) { + return lowerFirst(removeSuffix(str, suffix)); + } + + /** + * 忽略大小写去掉指定后缀 + * + * @param str 字符串 + * @param suffix 后缀 + * @return 切掉后的字符串,若后缀不是 suffix, 返回原字符串 + */ + public static String removeSuffixIgnoreCase(CharSequence str, CharSequence suffix) { + if (isEmpty(str) || isEmpty(suffix)) { + return StringPool.EMPTY; + } + + final String str2 = str.toString(); + if (str2.toLowerCase().endsWith(suffix.toString().toLowerCase())) { + return subPre(str2, str2.length() - suffix.length()); + } + return str2; + } + + /** + * 首字母变小写 + * @param str 字符串 + * @return {String} + */ + public static String lowerFirst(String str) { + char firstChar = str.charAt(0); + if (firstChar >= 'A' && firstChar <= 'Z') { + char[] arr = str.toCharArray(); + arr[0] += ('a' - 'A'); + return new String(arr); + } + return str; + } + + /** + * 首字母变大写 + * @param str 字符串 + * @return {String} + */ + public static String upperFirst(String str) { + char firstChar = str.charAt(0); + if (firstChar >= 'a' && firstChar <= 'z') { + char[] arr = str.toCharArray(); + arr[0] -= ('a' - 'A'); + return new String(arr); + } + return str; + } + + /** + * 切割指定位置之前部分的字符串 + * + * @param string 字符串 + * @param toIndex 切割到的位置(不包括) + * @return 切割后的剩余的前半部分字符串 + */ + public static String subPre(CharSequence string, int toIndex) { + return sub(string, 0, toIndex); + } + + /** + * 切割指定位置之后部分的字符串 + * + * @param string 字符串 + * @param fromIndex 切割开始的位置(包括) + * @return 切割后后剩余的后半部分字符串 + */ + public static String subSuf(CharSequence string, int fromIndex) { + if (isEmpty(string)) { + return null; + } + return sub(string, fromIndex, string.length()); + } + + /** + * 指定范围内查找指定字符 + * + * @param str 字符串 + * @param searchChar 被查找的字符 + * @return 位置 + */ + public static int indexOf(final CharSequence str, char searchChar) { + return indexOf(str, searchChar, 0); + } + + /** + * 指定范围内查找指定字符 + * + * @param str 字符串 + * @param searchChar 被查找的字符 + * @param start 起始位置,如果小于0,从0开始查找 + * @return 位置 + */ + public static int indexOf(final CharSequence str, char searchChar, int start) { + if (str instanceof String) { + return ((String) str).indexOf(searchChar, start); + } else { + return indexOf(str, searchChar, start, -1); + } + } + + /** + * 指定范围内查找指定字符 + * + * @param str 字符串 + * @param searchChar 被查找的字符 + * @param start 起始位置,如果小于0,从0开始查找 + * @param end 终止位置,如果超过str.length()则默认查找到字符串末尾 + * @return 位置 + */ + public static int indexOf(final CharSequence str, char searchChar, int start, int end) { + final int len = str.length(); + if (start < 0 || start > len) { + start = 0; + } + if (end > len || end < 0) { + end = len; + } + for (int i = start; i < end; i++) { + if (str.charAt(i) == searchChar) { + return i; + } + } + return -1; + } + + /** + * 指定范围内查找字符串,忽略大小写
+ * + *
+     * StringUtil.indexOfIgnoreCase(null, *, *)          = -1
+     * StringUtil.indexOfIgnoreCase(*, null, *)          = -1
+     * StringUtil.indexOfIgnoreCase("", "", 0)           = 0
+     * StringUtil.indexOfIgnoreCase("aabaabaa", "A", 0)  = 0
+     * StringUtil.indexOfIgnoreCase("aabaabaa", "B", 0)  = 2
+     * StringUtil.indexOfIgnoreCase("aabaabaa", "AB", 0) = 1
+     * StringUtil.indexOfIgnoreCase("aabaabaa", "B", 3)  = 5
+     * StringUtil.indexOfIgnoreCase("aabaabaa", "B", 9)  = -1
+     * StringUtil.indexOfIgnoreCase("aabaabaa", "B", -1) = 2
+     * StringUtil.indexOfIgnoreCase("aabaabaa", "", 2)   = 2
+     * StringUtil.indexOfIgnoreCase("abc", "", 9)        = -1
+     * 
+ * + * @param str 字符串 + * @param searchStr 需要查找位置的字符串 + * @return 位置 + * @since 3.2.1 + */ + public static int indexOfIgnoreCase(final CharSequence str, final CharSequence searchStr) { + return indexOfIgnoreCase(str, searchStr, 0); + } + + /** + * 指定范围内查找字符串 + * + *
+     * StringUtil.indexOfIgnoreCase(null, *, *)          = -1
+     * StringUtil.indexOfIgnoreCase(*, null, *)          = -1
+     * StringUtil.indexOfIgnoreCase("", "", 0)           = 0
+     * StringUtil.indexOfIgnoreCase("aabaabaa", "A", 0)  = 0
+     * StringUtil.indexOfIgnoreCase("aabaabaa", "B", 0)  = 2
+     * StringUtil.indexOfIgnoreCase("aabaabaa", "AB", 0) = 1
+     * StringUtil.indexOfIgnoreCase("aabaabaa", "B", 3)  = 5
+     * StringUtil.indexOfIgnoreCase("aabaabaa", "B", 9)  = -1
+     * StringUtil.indexOfIgnoreCase("aabaabaa", "B", -1) = 2
+     * StringUtil.indexOfIgnoreCase("aabaabaa", "", 2)   = 2
+     * StringUtil.indexOfIgnoreCase("abc", "", 9)        = -1
+     * 
+ * + * @param str 字符串 + * @param searchStr 需要查找位置的字符串 + * @param fromIndex 起始位置 + * @return 位置 + * @since 3.2.1 + */ + public static int indexOfIgnoreCase(final CharSequence str, final CharSequence searchStr, int fromIndex) { + return indexOf(str, searchStr, fromIndex, true); + } + + /** + * 指定范围内反向查找字符串 + * + * @param str 字符串 + * @param searchStr 需要查找位置的字符串 + * @param fromIndex 起始位置 + * @param ignoreCase 是否忽略大小写 + * @return 位置 + * @since 3.2.1 + */ + public static int indexOf(final CharSequence str, CharSequence searchStr, int fromIndex, boolean ignoreCase) { + if (str == null || searchStr == null) { + return INDEX_NOT_FOUND; + } + if (fromIndex < 0) { + fromIndex = 0; + } + + final int endLimit = str.length() - searchStr.length() + 1; + if (fromIndex > endLimit) { + return INDEX_NOT_FOUND; + } + if (searchStr.length() == 0) { + return fromIndex; + } + + if (false == ignoreCase) { + // 不忽略大小写调用JDK方法 + return str.toString().indexOf(searchStr.toString(), fromIndex); + } + + for (int i = fromIndex; i < endLimit; i++) { + if (isSubEquals(str, i, searchStr, 0, searchStr.length(), true)) { + return i; + } + } + return INDEX_NOT_FOUND; + } + + /** + * 指定范围内查找字符串,忽略大小写
+ * + * @param str 字符串 + * @param searchStr 需要查找位置的字符串 + * @return 位置 + * @since 3.2.1 + */ + public static int lastIndexOfIgnoreCase(final CharSequence str, final CharSequence searchStr) { + return lastIndexOfIgnoreCase(str, searchStr, str.length()); + } + + /** + * 指定范围内查找字符串,忽略大小写
+ * + * @param str 字符串 + * @param searchStr 需要查找位置的字符串 + * @param fromIndex 起始位置,从后往前计数 + * @return 位置 + * @since 3.2.1 + */ + public static int lastIndexOfIgnoreCase(final CharSequence str, final CharSequence searchStr, int fromIndex) { + return lastIndexOf(str, searchStr, fromIndex, true); + } + + /** + * 指定范围内查找字符串
+ * + * @param str 字符串 + * @param searchStr 需要查找位置的字符串 + * @param fromIndex 起始位置,从后往前计数 + * @param ignoreCase 是否忽略大小写 + * @return 位置 + * @since 3.2.1 + */ + public static int lastIndexOf(final CharSequence str, final CharSequence searchStr, int fromIndex, boolean ignoreCase) { + if (str == null || searchStr == null) { + return INDEX_NOT_FOUND; + } + if (fromIndex < 0) { + fromIndex = 0; + } + fromIndex = Math.min(fromIndex, str.length()); + + if (searchStr.length() == 0) { + return fromIndex; + } + + if (false == ignoreCase) { + // 不忽略大小写调用JDK方法 + return str.toString().lastIndexOf(searchStr.toString(), fromIndex); + } + + for (int i = fromIndex; i > 0; i--) { + if (isSubEquals(str, i, searchStr, 0, searchStr.length(), true)) { + return i; + } + } + return INDEX_NOT_FOUND; + } + + /** + * 返回字符串 searchStr 在字符串 str 中第 ordinal 次出现的位置。
+ * 如果 str=null 或 searchStr=null 或 ordinal<=0 则返回-1
+ * 此方法来自:Apache-Commons-Lang + *

+ * 栗子(*代表任意字符): + * + *

+     * StringUtil.ordinalIndexOf(null, *, *)          = -1
+     * StringUtil.ordinalIndexOf(*, null, *)          = -1
+     * StringUtil.ordinalIndexOf("", "", *)           = 0
+     * StringUtil.ordinalIndexOf("aabaabaa", "a", 1)  = 0
+     * StringUtil.ordinalIndexOf("aabaabaa", "a", 2)  = 1
+     * StringUtil.ordinalIndexOf("aabaabaa", "b", 1)  = 2
+     * StringUtil.ordinalIndexOf("aabaabaa", "b", 2)  = 5
+     * StringUtil.ordinalIndexOf("aabaabaa", "ab", 1) = 1
+     * StringUtil.ordinalIndexOf("aabaabaa", "ab", 2) = 4
+     * StringUtil.ordinalIndexOf("aabaabaa", "", 1)   = 0
+     * StringUtil.ordinalIndexOf("aabaabaa", "", 2)   = 0
+     * 
+ * + * @param str 被检查的字符串,可以为null + * @param searchStr 被查找的字符串,可以为null + * @param ordinal 第几次出现的位置 + * @return 查找到的位置 + * @since 3.2.3 + */ + public static int ordinalIndexOf(String str, String searchStr, int ordinal) { + if (str == null || searchStr == null || ordinal <= 0) { + return INDEX_NOT_FOUND; + } + if (searchStr.length() == 0) { + return 0; + } + int found = 0; + int index = INDEX_NOT_FOUND; + do { + index = str.indexOf(searchStr, index + 1); + if (index < 0) { + return index; + } + found++; + } while (found < ordinal); + return index; + } + + /** + * 截取两个字符串的不同部分(长度一致),判断截取的子串是否相同
+ * 任意一个字符串为null返回false + * + * @param str1 第一个字符串 + * @param start1 第一个字符串开始的位置 + * @param str2 第二个字符串 + * @param start2 第二个字符串开始的位置 + * @param length 截取长度 + * @param ignoreCase 是否忽略大小写 + * @return 子串是否相同 + * @since 3.2.1 + */ + public static boolean isSubEquals(CharSequence str1, int start1, CharSequence str2, int start2, int length, boolean ignoreCase) { + if (null == str1 || null == str2) { + return false; + } + + return str1.toString().regionMatches(ignoreCase, start1, str2.toString(), start2, length); + } + + /** + * 比较两个字符串(大小写敏感)。 + * + *
+     * equalsIgnoreCase(null, null)   = true
+     * equalsIgnoreCase(null, "abc")  = false
+     * equalsIgnoreCase("abc", null)  = false
+     * equalsIgnoreCase("abc", "abc") = true
+     * equalsIgnoreCase("abc", "ABC") = true
+     * 
+ * + * @param str1 要比较的字符串1 + * @param str2 要比较的字符串2 + * @return 如果两个字符串相同,或者都是null,则返回true + */ + public static boolean equals(CharSequence str1, CharSequence str2) { + return equals(str1, str2, false); + } + + /** + * 比较两个字符串(大小写不敏感)。 + * + *
+     * equalsIgnoreCase(null, null)   = true
+     * equalsIgnoreCase(null, "abc")  = false
+     * equalsIgnoreCase("abc", null)  = false
+     * equalsIgnoreCase("abc", "abc") = true
+     * equalsIgnoreCase("abc", "ABC") = true
+     * 
+ * + * @param str1 要比较的字符串1 + * @param str2 要比较的字符串2 + * @return 如果两个字符串相同,或者都是null,则返回true + */ + public static boolean equalsIgnoreCase(CharSequence str1, CharSequence str2) { + return equals(str1, str2, true); + } + + /** + * 比较两个字符串是否相等。 + * + * @param str1 要比较的字符串1 + * @param str2 要比较的字符串2 + * @param ignoreCase 是否忽略大小写 + * @return 如果两个字符串相同,或者都是null,则返回true + * @since 3.2.0 + */ + public static boolean equals(CharSequence str1, CharSequence str2, boolean ignoreCase) { + if (null == str1) { + // 只有两个都为null才判断相等 + return str2 == null; + } + if (null == str2) { + // 字符串2空,字符串1非空,直接false + return false; + } + + if (ignoreCase) { + return str1.toString().equalsIgnoreCase(str2.toString()); + } else { + return str1.equals(str2); + } + } + + /** + * 创建StringBuilder对象 + * + * @return StringBuilder对象 + */ + public static StringBuilder builder() { + return new StringBuilder(); + } + + /** + * 创建StringBuilder对象 + * + * @param capacity 初始大小 + * @return StringBuilder对象 + */ + public static StringBuilder builder(int capacity) { + return new StringBuilder(capacity); + } + + /** + * 创建StringBuilder对象 + * + * @param strs 初始字符串列表 + * @return StringBuilder对象 + */ + public static StringBuilder builder(CharSequence... strs) { + final StringBuilder sb = new StringBuilder(); + for (CharSequence str : strs) { + sb.append(str); + } + return sb; + } + + /** + * 创建StringBuilder对象 + * + * @param strs 初始字符串列表 + * @return StringBuilder对象 + */ + public static StringBuilder appendBuilder(StringBuilder sb, CharSequence... strs) { + for (CharSequence str : strs) { + sb.append(str); + } + return sb; + } + + /** + * 获得StringReader + * + * @param str 字符串 + * @return StringReader + */ + public static StringReader getReader(CharSequence str) { + if (null == str) { + return null; + } + return new StringReader(str.toString()); + } + + /** + * 获得StringWriter + * + * @return StringWriter + */ + public static StringWriter getWriter() { + return new StringWriter(); + } + + /** + * 统计指定内容中包含指定字符串的数量
+ * 参数为 {@code null} 或者 "" 返回 {@code 0}. + * + *
+     * StringUtil.count(null, *)       = 0
+     * StringUtil.count("", *)         = 0
+     * StringUtil.count("abba", null)  = 0
+     * StringUtil.count("abba", "")    = 0
+     * StringUtil.count("abba", "a")   = 2
+     * StringUtil.count("abba", "ab")  = 1
+     * StringUtil.count("abba", "xxx") = 0
+     * 
+ * + * @param content 被查找的字符串 + * @param strForSearch 需要查找的字符串 + * @return 查找到的个数 + */ + public static int count(CharSequence content, CharSequence strForSearch) { + if (Func.hasEmpty(content, strForSearch) || strForSearch.length() > content.length()) { + return 0; + } + + int count = 0; + int idx = 0; + final String content2 = content.toString(); + final String strForSearch2 = strForSearch.toString(); + while ((idx = content2.indexOf(strForSearch2, idx)) > -1) { + count++; + idx += strForSearch.length(); + } + return count; + } + + /** + * 统计指定内容中包含指定字符的数量 + * + * @param content 内容 + * @param charForSearch 被统计的字符 + * @return 包含数量 + */ + public static int count(CharSequence content, char charForSearch) { + int count = 0; + if (isEmpty(content)) { + return 0; + } + int contentLength = content.length(); + for (int i = 0; i < contentLength; i++) { + if (charForSearch == content.charAt(i)) { + count++; + } + } + return count; + } + + +} + diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/utils/SuffixFileFilter.java b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/SuffixFileFilter.java new file mode 100644 index 0000000..a8d4387 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/SuffixFileFilter.java @@ -0,0 +1,59 @@ +/** + * Copyright (c) 2018-2028, DreamLu 卢春梦 (qq596392912@gmail.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.tool.utils; + +import org.springframework.util.Assert; + +import java.io.File; +import java.io.FileFilter; +import java.io.Serializable; + +/** + * 文件后缀过滤器 + */ +public class SuffixFileFilter implements FileFilter, Serializable { + + private static final long serialVersionUID = -3389157631240246157L; + + private final String[] suffixes; + + public SuffixFileFilter(final String suffix) { + Assert.notNull(suffix, "The suffix must not be null"); + this.suffixes = new String[] {suffix}; + } + + public SuffixFileFilter(final String[] suffixes) { + Assert.notNull(suffixes, "The suffix must not be null"); + this.suffixes = new String[suffixes.length]; + System.arraycopy(suffixes, 0, this.suffixes, 0, suffixes.length); + } + + @Override + public boolean accept(File pathname) { + final String name = pathname.getName(); + for (final String suffix : this.suffixes) { + if (checkEndsWith(name, suffix)) { + return true; + } + } + return false; + } + + public boolean checkEndsWith(final String str, final String end) { + final int endLen = end.length(); + return str.regionMatches(true, str.length() - endLen, end, 0, endLen); + } +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/utils/URLUtil.java b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/URLUtil.java new file mode 100644 index 0000000..a46dec5 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/URLUtil.java @@ -0,0 +1,66 @@ +/** + * Copyright (c) 2018-2028, DreamLu 卢春梦 (qq596392912@gmail.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.tool.utils; + +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.Charset; + +/** + * url处理工具类 + */ +public class URLUtil extends org.springframework.web.util.UriUtils { + + /** + * url 编码,同js decodeURIComponent + * + * @param source url + * @param charset 字符集 + * @return 编码后的url + */ + public static String encodeURL(String source, Charset charset) { + return URLUtil.encode(source, charset.name()); + } + + /** + * url 解码 + * + * @param source url + * @param charset 字符集 + * @return 解码url + */ + public static String decodeURL(String source, Charset charset) { + return URLUtil.decode(source, charset.name()); + } + + /** + * 获取url路径 + * @param uriStr + * @return + */ + public static String getPath(String uriStr) { + URI uri; + + try { + uri = new URI(uriStr); + } catch (URISyntaxException var3) { + throw new RuntimeException(var3); + } + + return uri.getPath(); + } + +} diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/utils/WebUtil.java b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/WebUtil.java new file mode 100644 index 0000000..62a8181 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/WebUtil.java @@ -0,0 +1,270 @@ +/** + * Copyright (c) 2018-2028, DreamLu 卢春梦 (qq596392912@gmail.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.tool.utils; + +import lombok.extern.slf4j.Slf4j; +import org.springblade.core.tool.jackson.JsonUtil; +import org.springframework.http.MediaType; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import org.springframework.web.method.HandlerMethod; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.Enumeration; + + +/** + * Miscellaneous utilities for web applications. + * + * @author L.cm + */ +@Slf4j +public class WebUtil extends org.springframework.web.util.WebUtils { + + public static final String USER_AGENT_HEADER = "user-agent"; + + /** + * 判断是否ajax请求 + * spring ajax 返回含有 ResponseBody 或者 RestController注解 + * + * @param handlerMethod HandlerMethod + * @return 是否ajax请求 + */ + public static boolean isBody(HandlerMethod handlerMethod) { + ResponseBody responseBody = ClassUtil.getAnnotation(handlerMethod, ResponseBody.class); + return responseBody != null; + } + + /** + * 读取cookie + * + * @param name cookie name + * @return cookie value + */ + @Nullable + public static String getCookieVal(String name) { + HttpServletRequest request = WebUtil.getRequest(); + Assert.notNull(request, "request from RequestContextHolder is null"); + return getCookieVal(request, name); + } + + /** + * 读取cookie + * + * @param request HttpServletRequest + * @param name cookie name + * @return cookie value + */ + @Nullable + public static String getCookieVal(HttpServletRequest request, String name) { + Cookie cookie = getCookie(request, name); + return cookie != null ? cookie.getValue() : null; + } + + /** + * 清除 某个指定的cookie + * + * @param response HttpServletResponse + * @param key cookie key + */ + public static void removeCookie(HttpServletResponse response, String key) { + setCookie(response, key, null, 0); + } + + /** + * 设置cookie + * + * @param response HttpServletResponse + * @param name cookie name + * @param value cookie value + * @param maxAgeInSeconds maxage + */ + public static void setCookie(HttpServletResponse response, String name, @Nullable String value, int maxAgeInSeconds) { + Cookie cookie = new Cookie(name, value); + cookie.setPath("/"); + cookie.setMaxAge(maxAgeInSeconds); + cookie.setHttpOnly(true); + response.addCookie(cookie); + } + + /** + * 获取 HttpServletRequest + * + * @return {HttpServletRequest} + */ + public static HttpServletRequest getRequest() { + RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); + return (requestAttributes == null) ? null : ((ServletRequestAttributes) requestAttributes).getRequest(); + } + + /** + * 返回json + * + * @param response HttpServletResponse + * @param result 结果对象 + */ + public static void renderJson(HttpServletResponse response, Object result) { + renderJson(response, result, MediaType.APPLICATION_JSON_UTF8_VALUE); + } + + /** + * 返回json + * + * @param response HttpServletResponse + * @param result 结果对象 + * @param contentType contentType + */ + public static void renderJson(HttpServletResponse response, Object result, String contentType) { + response.setCharacterEncoding("UTF-8"); + response.setContentType(contentType); + try (PrintWriter out = response.getWriter()) { + out.append(JsonUtil.toJson(result)); + } catch (IOException e) { + log.error(e.getMessage(), e); + } + } + + /** + * 获取ip + * + * @return {String} + */ + public static String getIP() { + return getIP(WebUtil.getRequest()); + } + + /** + * 获取ip + * + * @param request HttpServletRequest + * @return {String} + */ + @Nullable + public static String getIP(HttpServletRequest request) { + Assert.notNull(request, "HttpServletRequest is null"); + String ip = request.getHeader("X-Requested-For"); + if (StringUtil.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("X-Forwarded-For"); + } + if (StringUtil.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("Proxy-Client-IP"); + } + if (StringUtil.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("WL-Proxy-Client-IP"); + } + if (StringUtil.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("HTTP_CLIENT_IP"); + } + if (StringUtil.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("HTTP_X_FORWARDED_FOR"); + } + if (StringUtil.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) { + ip = request.getRemoteAddr(); + } + return StringUtil.isBlank(ip) ? null : ip.split(",")[0]; + } + + + /*** + * 获取 request 中 json 字符串的内容 + * + * @param request + * @throws IOException + */ + public static String getRequestParamString(HttpServletRequest request) { + try { + return getRequestStr(request); + } catch (Exception ex) { + return StringPool.EMPTY; + } + } + + /** + * 获取 request 请求内容 + * + * @param request + * @return + * @throws IOException + */ + public static String getRequestStr(HttpServletRequest request) throws IOException { + String queryString = request.getQueryString(); + if (StringUtil.isNotBlank(queryString)) { + return new String(queryString.getBytes(Charsets.ISO_8859_1), Charsets.UTF_8).replaceAll("&", "&").replaceAll("%22", "\""); + } + return getRequestStr(request, getRequestBytes(request)); + } + + /** + * 获取 request 请求的 byte[] 数组 + * + * @param request + * @return + * @throws IOException + */ + public static byte[] getRequestBytes(HttpServletRequest request) throws IOException { + int contentLength = request.getContentLength(); + if (contentLength < 0) { + return null; + } + byte buffer[] = new byte[contentLength]; + for (int i = 0; i < contentLength; ) { + + int readlen = request.getInputStream().read(buffer, i, contentLength - i); + if (readlen == -1) { + break; + } + i += readlen; + } + return buffer; + } + + /** + * 获取 request 请求内容 + * @param request + * @param buffer + * @return + * @throws IOException + */ + public static String getRequestStr(HttpServletRequest request, byte buffer[]) throws IOException { + String charEncoding = request.getCharacterEncoding(); + if (charEncoding == null) { + charEncoding = StringPool.UTF_8; + } + String str = new String(buffer, charEncoding).trim(); + if (StringUtil.isBlank(str)) { + StringBuilder sb = new StringBuilder(); + Enumeration parameterNames = request.getParameterNames(); + while (parameterNames.hasMoreElements()) { + String key = parameterNames.nextElement(); + String value = request.getParameter(key); + StringUtil.appendBuilder(sb, key, "=", value, "&"); + } + str = StringUtil.removeSuffix(sb.toString(), "&"); + } + return str.replaceAll("&", "&"); + } + + +} + diff --git a/blade-core-tool/src/main/java/org/springblade/core/tool/utils/XmlUtil.java b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/XmlUtil.java new file mode 100644 index 0000000..0e9e9e5 --- /dev/null +++ b/blade-core-tool/src/main/java/org/springblade/core/tool/utils/XmlUtil.java @@ -0,0 +1,267 @@ +/** + * Copyright (c) 2018-2028, DreamLu 卢春梦 (qq596392912@gmail.com). + *

+ * Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springblade.core.tool.utils; + +import org.springframework.lang.Nullable; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + +import javax.xml.namespace.QName; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpressionException; +import javax.xml.xpath.XPathFactory; +import java.io.IOException; +import java.io.InputStream; +import java.io.StringReader; +import java.util.HashMap; +import java.util.Map; + +/** + * xpath解析xml + * + *

+ *     文档地址:
+ *     http://www.w3school.com.cn/xpath/index.asp
+ * 
+ * + * @author L.cm + */ +public class XmlUtil { + private final XPath path; + private final Document doc; + + private XmlUtil(InputSource inputSource) throws ParserConfigurationException, SAXException, IOException { + DocumentBuilderFactory dbf = getDocumentBuilderFactory(); + DocumentBuilder db = dbf.newDocumentBuilder(); + doc = db.parse(inputSource); + path = getXPathFactory().newXPath(); + } + + private static XmlUtil create(InputSource inputSource) { + try { + return new XmlUtil(inputSource); + } catch (ParserConfigurationException | SAXException | IOException e) { + throw Exceptions.unchecked(e); + } + } + + public static XmlUtil of(InputStream is) { + InputSource inputSource = new InputSource(is); + return create(inputSource); + } + + public static XmlUtil of(String xmlStr) { + StringReader sr = new StringReader(xmlStr.trim()); + InputSource inputSource = new InputSource(sr); + XmlUtil xmlUtil = create(inputSource); + IOUtil.closeQuietly(sr); + return xmlUtil; + } + + private Object evalXPath(String expression, @Nullable Object item, QName returnType) { + item = null == item ? doc : item; + try { + return path.evaluate(expression, item, returnType); + } catch (XPathExpressionException e) { + throw Exceptions.unchecked(e); + } + } + + /** + * 获取String + * + * @param expression 路径 + * @return String + */ + public String getString(String expression) { + return (String) evalXPath(expression, null, XPathConstants.STRING); + } + + /** + * 获取Boolean + * + * @param expression 路径 + * @return String + */ + public Boolean getBoolean(String expression) { + return (Boolean) evalXPath(expression, null, XPathConstants.BOOLEAN); + } + + /** + * 获取Number + * + * @param expression 路径 + * @return {Number} + */ + public Number getNumber(String expression) { + return (Number) evalXPath(expression, null, XPathConstants.NUMBER); + } + + /** + * 获取某个节点 + * + * @param expression 路径 + * @return {Node} + */ + public Node getNode(String expression) { + return (Node) evalXPath(expression, null, XPathConstants.NODE); + } + + /** + * 获取子节点 + * + * @param expression 路径 + * @return NodeList + */ + public NodeList getNodeList(String expression) { + return (NodeList) evalXPath(expression, null, XPathConstants.NODESET); + } + + + /** + * 获取String + * + * @param node 节点 + * @param expression 相对于node的路径 + * @return String + */ + public String getString(Object node, String expression) { + return (String) evalXPath(expression, node, XPathConstants.STRING); + } + + /** + * 获取 + * + * @param node 节点 + * @param expression 相对于node的路径 + * @return String + */ + public Boolean getBoolean(Object node, String expression) { + return (Boolean) evalXPath(expression, node, XPathConstants.BOOLEAN); + } + + /** + * 获取 + * + * @param node 节点 + * @param expression 相对于node的路径 + * @return {Number} + */ + public Number getNumber(Object node, String expression) { + return (Number) evalXPath(expression, node, XPathConstants.NUMBER); + } + + /** + * 获取某个节点 + * + * @param node 节点 + * @param expression 路径 + * @return {Node} + */ + public Node getNode(Object node, String expression) { + return (Node) evalXPath(expression, node, XPathConstants.NODE); + } + + /** + * 获取子节点 + * + * @param node 节点 + * @param expression 相对于node的路径 + * @return NodeList + */ + public NodeList getNodeList(Object node, String expression) { + return (NodeList) evalXPath(expression, node, XPathConstants.NODESET); + } + + /** + * 针对没有嵌套节点的简单处理 + * + * @return map集合 + */ + public Map toMap() { + Element root = doc.getDocumentElement(); + Map params = new HashMap(); + + // 将节点封装成map形式 + NodeList list = root.getChildNodes(); + for (int i = 0; i < list.getLength(); i++) { + Node node = list.item(i); + if (node instanceof Element) { + params.put(node.getNodeName(), node.getTextContent()); + } + } + return params; + } + + private static volatile boolean preventedXXE = false; + + private static DocumentBuilderFactory getDocumentBuilderFactory() throws ParserConfigurationException { + DocumentBuilderFactory dbf = XmlUtil.XmlHelperHolder.documentBuilderFactory; + if (!preventedXXE) { + preventXXE(dbf); + } + return dbf; + } + + // https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=23_5 + private static void preventXXE(DocumentBuilderFactory dbf) throws ParserConfigurationException { + // This is the PRIMARY defense. If DTDs (doctypes) are disallowed, almost all XML entity attacks are prevented + // Xerces 2 only - http://xerces.apache.org/xerces2-j/features.html#disallow-doctype-decl + dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); + + // If you can't completely disable DTDs, then at least do the following: + // Xerces 1 - http://xerces.apache.org/xerces-j/features.html#external-general-entities + // Xerces 2 - http://xerces.apache.org/xerces2-j/features.html#external-general-entities + + // JDK7+ - http://xml.org/sax/features/external-general-entities + dbf.setFeature("http://xml.org/sax/features/external-general-entities", false); + + // Xerces 1 - http://xerces.apache.org/xerces-j/features.html#external-parameter-entities + // Xerces 2 - http://xerces.apache.org/xerces2-j/features.html#external-parameter-entities + + // JDK7+ - http://xml.org/sax/features/external-parameter-entities + dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false); + + // Disable external DTDs as well + dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); + + // and these as well, per Timothy Morgan's 2014 paper: "XML Schema, DTD, and Entity Attacks" + dbf.setXIncludeAware(false); + dbf.setExpandEntityReferences(false); + preventedXXE = true; + } + + private static XPathFactory getXPathFactory() { + return XmlUtil.XmlHelperHolder.xPathFactory; + } + + /** + * 内部类单例 + */ + private static class XmlHelperHolder { + private static DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); + private static XPathFactory xPathFactory = XPathFactory.newInstance(); + } + +} diff --git a/blade-core-tool/src/main/resources/META-INF/spring-devtools.properties b/blade-core-tool/src/main/resources/META-INF/spring-devtools.properties new file mode 100644 index 0000000..b252d0a --- /dev/null +++ b/blade-core-tool/src/main/resources/META-INF/spring-devtools.properties @@ -0,0 +1 @@ +restart.include.blade-core-tool=/blade-core-tool[\\w-]+\.jar diff --git a/blade-core-tool/src/main/resources/META-INF/spring.factories b/blade-core-tool/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..e9d7490 --- /dev/null +++ b/blade-core-tool/src/main/resources/META-INF/spring.factories @@ -0,0 +1,4 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + org.springblade.core.tool.config.ToolConfiguration,\ + org.springblade.core.tool.config.JacksonConfiguration,\ + org.springblade.core.tool.config.MessageConfiguration diff --git a/pic/jx.png b/pic/jx.png new file mode 100644 index 0000000..4557523 Binary files /dev/null and b/pic/jx.png differ diff --git a/pic/springblade-admin1.png b/pic/springblade-admin1.png new file mode 100644 index 0000000..be362ac Binary files /dev/null and b/pic/springblade-admin1.png differ diff --git a/pic/springblade-admin2.png b/pic/springblade-admin2.png new file mode 100644 index 0000000..cbfca54 Binary files /dev/null and b/pic/springblade-admin2.png differ diff --git a/pic/springblade-consul-nodes1.png b/pic/springblade-consul-nodes1.png new file mode 100644 index 0000000..ebd73bf Binary files /dev/null and b/pic/springblade-consul-nodes1.png differ diff --git a/pic/springblade-consul-nodes2.png b/pic/springblade-consul-nodes2.png new file mode 100644 index 0000000..3cc105e Binary files /dev/null and b/pic/springblade-consul-nodes2.png differ diff --git a/pic/springblade-consul.png b/pic/springblade-consul.png new file mode 100644 index 0000000..24571ea Binary files /dev/null and b/pic/springblade-consul.png differ diff --git a/pic/springblade-harbor.png b/pic/springblade-harbor.png new file mode 100644 index 0000000..5611bb4 Binary files /dev/null and b/pic/springblade-harbor.png differ diff --git a/pic/springblade-k8s.png b/pic/springblade-k8s.png new file mode 100644 index 0000000..bdf3641 Binary files /dev/null and b/pic/springblade-k8s.png differ diff --git a/pic/springblade-swagger1.png b/pic/springblade-swagger1.png new file mode 100644 index 0000000..5ac6e90 Binary files /dev/null and b/pic/springblade-swagger1.png differ diff --git a/pic/springblade-swagger2.png b/pic/springblade-swagger2.png new file mode 100644 index 0000000..66a8165 Binary files /dev/null and b/pic/springblade-swagger2.png differ diff --git a/pic/springblade-traefik-health.png b/pic/springblade-traefik-health.png new file mode 100644 index 0000000..733172f Binary files /dev/null and b/pic/springblade-traefik-health.png differ diff --git a/pic/springblade-traefik.png b/pic/springblade-traefik.png new file mode 100644 index 0000000..ccd95b2 Binary files /dev/null and b/pic/springblade-traefik.png differ diff --git a/pic/sword-dict.png b/pic/sword-dict.png new file mode 100644 index 0000000..89f8ed3 Binary files /dev/null and b/pic/sword-dict.png differ diff --git a/pic/sword-locale-cn.png b/pic/sword-locale-cn.png new file mode 100644 index 0000000..b832ae3 Binary files /dev/null and b/pic/sword-locale-cn.png differ diff --git a/pic/sword-locale-us.png b/pic/sword-locale-us.png new file mode 100644 index 0000000..ebb8f32 Binary files /dev/null and b/pic/sword-locale-us.png differ diff --git a/pic/sword-log.png b/pic/sword-log.png new file mode 100644 index 0000000..08caf63 Binary files /dev/null and b/pic/sword-log.png differ diff --git a/pic/sword-menu-edit.png b/pic/sword-menu-edit.png new file mode 100644 index 0000000..a3bd0a1 Binary files /dev/null and b/pic/sword-menu-edit.png differ diff --git a/pic/sword-menu-icon.png b/pic/sword-menu-icon.png new file mode 100644 index 0000000..ccc0c58 Binary files /dev/null and b/pic/sword-menu-icon.png differ diff --git a/pic/sword-menu.png b/pic/sword-menu.png new file mode 100644 index 0000000..43ee030 Binary files /dev/null and b/pic/sword-menu.png differ diff --git a/pic/sword-role.png b/pic/sword-role.png new file mode 100644 index 0000000..3f640e2 Binary files /dev/null and b/pic/sword-role.png differ diff --git a/pic/sword-user.png b/pic/sword-user.png new file mode 100644 index 0000000..3414a33 Binary files /dev/null and b/pic/sword-user.png differ diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..bb9ac04 --- /dev/null +++ b/pom.xml @@ -0,0 +1,204 @@ + + + 4.0.0 + + org.springblade + blade-tool + 1.0.0-RC1 + pom + + + 1.0.0-RC1 + + 1.8 + 3.8.0 + 2.9.2 + 1.5.21 + 1.8.8 + 3.0.6 + 4.0.1 + 1.6.0 + 3.4.2 + 2.0.2 + + 2.0.7.RELEASE + Finchley.SR2 + Cairo-SR5 + + + + blade-core-boot + blade-core-launch + blade-core-secure + blade-core-tool + blade-core-log + blade-core-mybatis + blade-core-swagger + + + + + + org.springframework.boot + spring-boot-dependencies + ${spring.boot.version} + pom + import + + + org.springframework.cloud + spring-cloud-dependencies + ${spring.cloud.version} + pom + import + + + io.spring.platform + platform-bom + ${spring.platform.version} + pom + import + + + + + + + org.springframework.boot + spring-boot-starter-aop + + + org.springframework.boot + spring-boot-starter-test + test + + + de.codecentric + spring-boot-admin-starter-client + ${spring.boot.admin.version} + + + org.springframework.boot + spring-boot-configuration-processor + true + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.cloud + spring-cloud-starter-netflix-hystrix + + + + org.springframework.cloud + spring-cloud-starter-consul-discovery + + + org.springframework.retry + spring-retry + + + org.projectlombok + lombok + provided + + + org.springframework.boot + spring-boot-devtools + true + + + + + + ${project.name} + + + src/main/resources + + + src/main/java + + **/*.xml + + + + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven.plugin.version} + + ${java.version} + ${java.version} + UTF-8 + + -parameters + + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.1.0 + + + + org.apache.maven.plugins + maven-source-plugin + 3.0.1 + + true + + + + compile + + jar + + + + + + + + + + aliyun-repos + http://maven.aliyun.com/nexus/content/groups/public/ + + false + + + + + + + aliyun-plugin + http://maven.aliyun.com/nexus/content/groups/public/ + + false + + + + + + + release + Release Repository + http://nexus.gitee.ltd/repository/maven-releases/ + + + +