WebSample For SpringFrame 的项目停止更新。转向以 Spring Boot + thymeleaf 的演示项目
提供 web 应用的一些实用功能框架
Version 3.1.0 支持 spring 2.1版本
Version 4.0.0 支持 spring 2.2版本
Version 4.1.0 文件上传的工具类支持 HDFS 方式的存放
- 业务方法调用记录到 log 表单的封装
- 错误信息捕获,并且进行 json 化和详细信息化的封装
- 短信验证码发送和验证功能的封装
- 访问 IP 权限白名单控制功能的封装
- mvc中 controller 层方法的 session 判断的封装
- 同时进行多文件和表单提交处理,并且自动保存文件的功能封装
- 文件http上传的时候,支持HDFS 的分布式存放。
- 基于 totp的二次验证方法类
- 增加对上传 webp 文件的时候,自动生成缩略图
-
在项目的 applicationContext.xml 文件中增加配置 jpa 扫描目录
<!-- 配置Spring Data JPA扫描目录 --> <jpa:repositories base-package="pers.roamer.boracay,xxx.xxxxx.xxx(项目的 jpa 扫描目录)"/>
-
在项目的 applicationContext.xml 文件中增加组件扫描目录,激活自动代理功能
<context:component-scan base-package="pers.roamer.boracay,xxxx.xxxxx.xxx"/> <!-- 激活自动代理功能 --> <aop:aspectj-autoproxy proxy-target-class="true"/>
-
在项目的 spring-mvc.xml 文件中增加 spring mvc 的包扫描目录
<context:component-scan base-package="pers.roamer.boracay,xxxx.xxxxx.xxx"/>
-
在项目的$classpath目录下增加config目录,并且增加config.xml 配置文件中,增加如下的项目配置项
<Config> <System> <!-- 用于 boracay 组件必须使用到的配置 begin--> <AppName> 项目名称 </AppName> <!--写入到数据表中的 created_by 和 update_by 系统用户--> <SystemAdminName> systemadmin </SystemAdminName> <!--访问白名单--> <!--# 系统能访问的IP白名单。只有在这个白名单里面的IP才能访问--> <!--#内网地址都纳入--> <!--#A类10.0.0.0--10.255.255.255--> <!--#B类172.16.0.0--172.31.255.255--> <!--#C类192.168.0.0--192.168.255.255--> <Whitelist> 127.0.0.1,192.168.0-255.0-255,172.16-31.0-255.0-255,10.0-255.0-255.0-255 </Whitelist> <!----> <!--设置是否要做session判断,以便确定用户是否登录--> <SessionCheck>false</SessionCheck> <!-- 设置是否要做业务活动日志记录 --> <RecordBusinessLog>true</RecordBusinessLog> <!----> <!-- 设置判断 session 中被用于保存用户信息的关键词 --> <SessionUserKeyword> user_mobile </SessionUserKeyword> <!-- 需要保存到日志记录数据库中的用户名字在 session 中保存的 key--> <BusinessRecordUserName> user_name </BusinessRecordUserName> <!-- 用于 boracay 组件必须使用到的配置 end --> </System> ... </Config>
-
在 applicationContext.xml 中增加如下代码:
<!-- 配置业务方法日志记录的功能 begin--> <bean id="businessLogInterceptor" class="pers.roamer.boracay.aspect.businesslogger.BusinessLogAspect"></bean> <aop:config> <aop:pointcut id="logControllerPointcut" ①expression="execution(* com.ninelephas.raccoon.controller..*.*(..)) && !execution(* com.ninelephas.raccoon.controller.creator.CreatorController.logout(..))" /> <aop:pointcut id="beforeLogControllerPointcut" ②expression="execution(* com.ninelephas.raccoon.controller.creator.CreatorController.logout(..))" /> <aop:aspect ref="businessLogInterceptor"> <aop:around method="logAroundAction" pointcut-ref="logControllerPointcut"/> <aop:before method="logBeforeAction" pointcut-ref="beforeLogControllerPointcut"/> </aop:aspect> </aop:config> <!-- 配置业务方法日志记录的功能 end -->
-
只有①和②的地方需要根据具体项目需求进行变更。
-
①的表达式是配置所有需要记录日志的 controller bean 中的方法,并且是排除了 logout 的方法。
-
②的表达式是配置所有需要在业务逻辑前进行日志记录的 controller bean 中的方法。典型的场景是 logout。
-
这些包含在表达式中的方法,如果用了 @BusinessMethod(value = "登录") 这样的方法注解。就会自动记录日志到日志表中。
-
可以在具体指定当前方法是否要日志记录,设置 isLogged = false 就可以对这个方法不记录日志。缺省是 true,代表记录日志。
@BusinessMethod(value = "登录" , isLogged = false )
-
如果整个项目,无论在controller 里面的业务方法里面是否指定 isLogged,都不需要记录日志。可以在 config.xml 里面的 RecordBusinessLog 设置成 false。
-
-
系统日志的表单结构:
CREATE TABLE `business_log` ( `id` varchar(40) CHARACTER SET utf8 NOT NULL, `operator` varchar(255) COLLATE utf8_bin DEFAULT NULL, `clazz` varchar(255) CHARACTER SET utf8 NOT NULL, `method` varchar(255) CHARACTER SET utf8 NOT NULL, `method_description` varchar(255) CHARACTER SET utf8 NOT NULL, `success` tinyint(4) NOT NULL COMMENT '方法是否成功运行', `exception_string` text COLLATE utf8_bin COMMENT '方法运行出错,抛出的exception堆栈转换成的string', `args` text CHARACTER SET utf8 NOT NULL, `time_consuming` bigint NOT NULL DEFAULT 0 COMMENT '方法调用耗时(毫秒)', `remote_ip` varchar(32) COLLATE utf8_bin DEFAULT NULL, `client_os` varchar(255) COLLATE utf8_bin DEFAULT NULL, `client_browser` varchar(255) COLLATE utf8_bin DEFAULT NULL, `browser_version` varchar(255) COLLATE utf8_bin DEFAULT NULL, `client_device_type` varchar(255) COLLATE utf8_bin DEFAULT NULL, `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin ROW_FORMAT=DYNAMIC COMMENT='业务方法调用日志'; SET FOREIGN_KEY_CHECKS = 1;
-
在 applicationContext.xml 中增加如下代码:
<!-- 配置controller 方法中抛出的错误进行 json封装,并且和 config.xml 中进行对应! begin--> <bean id="catchControllerExceptionAspect" class="pers.roamer.boracay.aspect.catchcontroller.CatchControllerExceptionAspect"></bean> <aop:config> <aop:pointcut id="catchControllerExceptionPointcut" expression="execution(* pers.roamer.boracay.controller..*.*(..)) || ①execution(* com.ninelephas.raccoon.controller..*.*(..))"/> <aop:aspect ref="catchControllerExceptionAspect"> <aop:after-throwing throwing="ex" method="writeToHttpResponse" pointcut-ref="catchControllerExceptionPointcut"/> </aop:aspect> </aop:config> <!-- 配置controller 方法中抛出的错误进行 json封装,并且和 config.xml 中进行对应! end-->
-
只有①的地方需要根据具体项目需求进行变更。
-
要对错误进行 json 话,需要在 url 后面加.json.否则还是返回标准的500错误的 stacktrace。
例子:
调用的 URL
http://127.0.0.1:8080/creator/rest/login.json
-
返回的错误数据信息如下:
{ "status": 500, "success": false, "message": "exception", "data": [ { "nodesName": [ "password" ], "errorMessage": "密码不匹配", "errorPath": "exception.creator.login.password_not_match" } ] }
其中:
-
errorStack 错误堆栈列表。为了便于后台程序员定位错误!
-
errorPath 是在 controller 中抛出的 ControllerException 的 message。
-
errorMessage: 是在 config.xml 中对 message 进行的国际化和信息明细化后的内容。
-
nodesName : 也是在 config.xml 中定义的.用于和前台的 dom 组件绑定。
-
-
config.xml 中的相关内容如下:
<!--定义错误信息--> <exception> <system> <need_login> 必须登录系统 </need_login> </system> <creator> <register> <mobile_used> 手机已经被注册过了!| mobile </mobile_used> <name_used> 用户名已经被注册过了!| passwd </name_used> </register> <login> <password_not_match> 密码不匹配|password </password_not_match> <user_not_exit> 无此用户|mobile </user_not_exit> <unHandle> 未捕获错误的登录失败 </unHandle> <mobile_is_null> 手机号为空 </mobile_is_null> <mobile_pattern_wrong> 手机号格式错误 </mobile_pattern_wrong> </login> <modify> <password> <validatecode_not_match> 修改密码的时候,验证码不正确!修改失败!| validate_code </validatecode_not_match> </password> </modify> <edit> <user_not_exit> 无此用户 | mobile </user_not_exit> <not_company_account> 非公司账户 </not_company_account> <not_personal_account> 非个人账户 </not_personal_account> </edit> <rebound_mobile> <old_mobile_not_exist> 需要解绑的手机号不存在,或者需要重新登录| oldMobile </old_mobile_not_exist> <new_mobile_used> 要重新绑定的手机号已经被注册了,不能被绑定| newMobile </new_mobile_used> </rebound_mobile> </creator>
-
在 applicationContext.xml 中增加如下代码:
<!-- 配置项目访问白名单功能 begin--> <bean id="whiteListCheckAspect" class="pers.roamer.boracay.aspect.whitelist.WhiteListCheckAspect"></bean> <aop:config> <aop:pointcut id="whiteListCheckPointcut" ①expression="execution(* com.ninelephas.raccoon.controller..*.*(..))"/> <aop:aspect ref="whiteListCheckAspect"> <aop:before method="whiteListCheck" pointcut-ref="whiteListCheckPointcut"/> </aop:aspect> </aop:config> <!-- 配置项目访问白名单功能 end-->
-
只有①的地方需要根据具体项目需求进行变更。
项目的 exception 继承结构建议
ProjectException extends BoracayException
ServiceExcpetion extends ProjectException
ControllerException extends ProjectException
service 层捕获所有的错误,重新封装成 ServiceException 抛出!
controller 层捕获所有的错误,重新封装成 ControllerException 抛出!
这样才能比较好的用到 boracay 组件里面的 优化Exception的功能。
-
短信验证码的存放数据表单
CREATE TABLE `sms_verification_code` ( `id` varchar(40) NOT NULL, `session_id` varchar(40) NOT NULL COMMENT 'session_id', `op_id` varchar(40) NOT NULL COMMENT '操作类型', `phone_number` varchar(40) NOT NULL COMMENT '手机号码', `text` varchar(255) NOT NULL COMMENT '短信内容', `duration` int(11) NOT NULL COMMENT '有效时长', `is_used` tinyint(1) NOT NULL COMMENT '是否被使用', `created_time` datetime NOT NULL COMMENT '创建时间', `used_time` datetime DEFAULT NULL COMMENT '使用时间', `code` varchar(255) NOT NULL COMMENT '验证码', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='短信验证码'; SET FOREIGN_KEY_CHECKS = 1;
-
配置文件
在 config/config.xml 文件中增加必须要的配置项
<!--短信接口--> <System> <Sms> <Url>http://222.73.117.158/msg/HttpBatchSendSM</Url> <Username>xxxxxx</Username> <Password>xxxxxx</Password> <BusinessMethod> <Regist> <!--注册验证码有效时长--> <Duration>1800000</Duration> <!--注册验证码长度--> <Length>6</Length> <Text>尊敬的用户,您注册的手机验证码为:${code},请填入以完成注册。该验证码有效时间为${duration}分钟,限本次使用。</Text> </Regist> <ForgetPassword> <!--忘记密码验证码有效时长--> <Duration>1800000</Duration> <!--忘记密码验证码长度--> <Length>6</Length> <Text>尊敬的用户,您正在使用忘记密码功能,您的手机验证码为:${code},该验证码有效时间为${duration}分钟,限本次使用。</Text> </ForgetPassword> <ModifyPhoneNumber> <!--修改手机号码验证码有效时长--> <Duration>1800000</Duration> <!--修改手机号码验证码长度--> <Length>6</Length> <Text>尊敬的用户,您正在使用修改手机号码功能,您的手机验证码为:${code},该验证码有效时间为${duration}分钟,限本次使用。</Text> </ModifyPhoneNumber> <ModifyPassword> <!--修改密码验证码有效时长--> <Duration>1800000</Duration> <!--修改密码验证码长度--> <Length>6</Length> <Text>尊敬的用户,您正在使用重置密码功能,您的手机验证码为:${code},该验证码有效时间为${duration}分钟,限本次使用。</Text> </ModifyPassword> </BusinessMethod> </Sms> </System> <!----> <exception> <!--短信验证部分--> <sms> <validate> <vcode> <not_set> 短信验证码没有设置! </not_set> <invalid> 短信验证码不存在! </invalid> <expired> 验证码过期! </expired> <not_match> 验证码不匹配! </not_match> </vcode> </validate> </sms> <!----> ... <exception>
-
切面配置
在 applicationContext.xml 中配置切面
<!-- ④配置项目中需要进行短信验证码验证的功能 begin--> <bean id="smsValidateCodeAspect" class="pers.roamer.boracay.aspect.sms.SMSValidateCodeAspect"></bean> <aop:config> <aop:pointcut id="smsValidateCodePointcut" ①expression="execution(* com.ninelephas.raccoon.controller..*.*(..))"/> <aop:aspect ref="smsValidateCodeAspect"> <aop:before method="smsValidateCodeCheck" pointcut-ref="smsValidateCodePointcut"/> </aop:aspect> </aop:config> <!-- ④配置项目中需要进行短信验证码验证的功能 end-->
-
只有①的地方,需要根据具体项目需求进行变更。
-
在需要进行短信验证的方法上加入注解
@SMSValidateCode(opId = "001")
其中的 opId 和生成短信验证码调用的时候定义的 opId一致
-
-
发送短信验证码的方法
http://127.0.0.1:8080/sms/send/{phoneNumber}/{opid}
-
在需要进行短信验证的controller方法上加入注解。这个方法要求传入发送在手机上的验证码。
使用 formData 的方法进行 controller 的调用。
例如:
传递的 formData 第一个参数(用于原来的逻辑) : creatorEntity { "mobile": "15800392200", "passwd": "passwd11" } 第二个参数(短信验证码的参数结构,除去验证码参数值和验证码发往的手机,其他都不能修改): smsValidateBean { "validateCode":"24234", "mobile", "158004932098" }
对应的 spring mvc 中的 controller 的方法定义如下:
@BusinessMethod(value = "注册") @SMSValidateMethod(opId = "001") @PostMapping(value = "/creator/rest/register") public String register(@RequestPart("creatorEntity") CreatorEntity creator , @RequestPart("smsValidateBean") SMSValidateBean smsValidateBean ) throws ControllerException { ... }
@SMSValidateMethod(opId = "001") 这个注解起到了对这个方法进行短信验证码验证的功能
-
这个切面可以支持一个方法需要对多个手机校验码的需求。例如 重新绑定手机的功能,就需要输入发往老手机号码的验证码和发往新手机号码的验证码。 调用方式: 在 controller方法上加入注解 @SMSValidateCode(opId = {"ReboundOldMobile","ReboundNewMobile"}) 同理,这几个 opID 也需要在 config.xml 中定义
-
切面配置 在 applicationContext.xml 中配置切面
<!-- ⑤配置项目中需要进行session check,确定是否登录的功能 begin--> <bean id="sessionCheckKeywordAspect" class="pers.roamer.boracay.aspect.httprequest.SessionCheckKeywordAspect"></bean> <aop:config> <aop:pointcut id="sessionKeywordCheckPointcut" expression="execution(* com.ninelephas.raccoon.controller..*.*(..))"/> <aop:aspect ref="sessionCheckKeywordAspect"> <aop:before method="sessionKeywordCheck" pointcut-ref="sessionKeywordCheckPointcut"/> </aop:aspect> </aop:config> <!-- ⑤配置项目中需要进行session check,确定是否登录的功能 end-->
-
只有①的地方,需要根据具体项目需求进行变更。
-
在controller 层的类上加入注解
@SessionCheckKeyword()
代表这个类下面的方法都需要做 登录认证。
-
如果要排除某个方法不需要登录认证。就在方法的上面加入注解 @SessionCheckKeyword(checkIt = false)
-
-
在 config/config.xml 文件中确定有必须要的配置项
System.SessionUserKeyword
-
在系统的登录功能中使用如下语句来设置 sessionCheck 的关键词
httpSession.setAttribute(ConfigHelper.getConfig().getString("System.SessionUserKeyword"), "要设置的值");
-
短发发送的方法类在 SmsController.java 里面
-
调用方法:
ArrayList<FileUploadResult> fileUploadResultArrayList1 = new UploadFileUtil().saveFile(files1, true);
第二个参数表示 是否把文件的摘要作为保存的文件的 id。 保存在服务器端目录结构: 是根据 config.xml中的 System.UploadFile.saveFilePath 部分所指定的路径+以ID为名字的目录+上传的文件名
#只需要在 Config.yaml 文件中的 System.UploadFile,配置如下代码:
HDFS:
Plugged: true
FileSystem: "hdfs://192.168.2.232:9000"
UserName: "brahma"
# HDFS 如不不配置,或者 Plugged: false .则不启用 HDFS 的存储功能。使用保存到本地物理硬盘的方法。保存路径通过 saveFilePath 指定
# 如果配置了 Plugged: true,则启用 HDFS。
# 保存路径通过 saveFilePath 指定在 HDFS 的根目录下
# FileSystem 和 UserName 指定 HDFS 的服务器访问地址和用户
context:
listener:
classes: pers.roamer.boracay.application.StartedListener
把 config/config.xml 文件保存在 spring boot项目下的/src/main/resources目录下。 确定 spring boot 项目启动后的控制台,有相应的info 信息产生。
......
17-09-21 10:48:43 INFO pers.roamer.boracay.application.StartedListener 65 行 onApplicationEvent - org.springframework.orm.jpa.SharedEntityManagerCreator#0
2017-09-21 10:48:43 INFO pers.roamer.boracay.application.StartedListener 67 行 onApplicationEvent - 所有被装备的Bean列表显示完成
2017-09-21 10:48:43 INFO pers.roamer.boracay.application.StartedListener 68 行 onApplicationEvent - 项目:[Raccoon Web Sample],启动完成
......
在 spring boot 的Application 的 class 中,增加 @ComponentScan("pers.roamer.boracay") @EnableJpaRepositories("pers.roamer.boracay") @EntityScan("pers.roamer.boracay.entity")
package pers.roamer.boracay.websample;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.ImportResource;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.ModelAndView;
@RestController
@SpringBootApplication // same as @Configuration @EnableAutoConfiguration @ComponentScan
@ImportResource(locations={"classpath:boracay-config.xml"})
@ComponentScan("pers.roamer.boracay")
@EnableJpaRepositories("pers.roamer.boracay")
@EntityScan("pers.roamer.boracay.entity")
public class BoracayWebSampleApplication {
public static void main(String[] args) {
SpringApplication.run(BoracayWebSampleApplication.class, args);
}
@RequestMapping("/")
public ModelAndView index() {
ModelAndView modelAndView = new ModelAndView("/index");
return modelAndView;
}
}
由于 Boracay 组件需要对各个项目的类包和类进行扫描和方法切入。这个配置是通过上面 spring framework 中的 applicationContext.xml 配置的。
同样,这些配置也必须给 spring boot 使用,
因此,在 resources 目录下建立一个 boracay-config.xml 文件。并且在 spring boot 的 主程序里面加入读取这个文件的配置
@ImportResource(locations={"classpath:boracay-config.xml"})
boracay-config.xml 内容如下:, 配置的详细功能和前面的一样。
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Boracay - Web 项目实用组件框架
~
~ @author 徐泽宇 [email protected]
~ @version 1.0.0
~ Copyright (c) 2017. 徐泽宇
~
-->
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns="http://www.springframework.org/schema/beans"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-4.3.xsd"
>
<!-- 配置徐泽宇的 boracay 框架要到的 aop begin-->
<!-- ①配置业务方法日志记录的功能 begin-->
<bean id="businessLogAspect" class="pers.roamer.boracay.aspect.businesslogger.BusinessLogAspect"></bean>
<aop:config>
<aop:pointcut
id="logControllerPointcut"
expression="execution(* pers.roamer.boracay.websample.controller..*.*(..)) && !execution(* pers.roamer.boracay.websample.controller.TestController.logout(..))"/>
<aop:pointcut
id="beforeLogControllerPointcut"
expression="execution(* pers.roamer.boracay.websample.controller.TestController.logout(..))"/>
<aop:aspect ref="businessLogAspect">
<aop:around method="logAroundAction" pointcut-ref="logControllerPointcut"/>
<aop:before method="logBeforeAction" pointcut-ref="beforeLogControllerPointcut"/>
</aop:aspect>
</aop:config>
<!-- ①配置业务方法日志记录的功能 end -->
<!-- ②配置controller 方法中抛出的错误进行 json封装,并且和 config.xml 中进行对应! begin-->
<bean id="catchControllerExceptionAspect"
class="pers.roamer.boracay.aspect.catchcontroller.CatchControllerExceptionAspect"></bean>
<aop:config>
<aop:pointcut
id="catchControllerExceptionPointcut"
expression="execution(* pers.roamer.boracay.controller..*.*(..)) || execution(* pers.roamer.boracay.websample.controller..*.*(..))"/>
<aop:aspect ref="catchControllerExceptionAspect">
<aop:after-throwing throwing="ex" method="writeToHttpResponse"
pointcut-ref="catchControllerExceptionPointcut"/>
</aop:aspect>
</aop:config>
<!-- ②配置controller 方法中抛出的错误进行 json封装,并且和 config.xml 中进行对应! end-->
<!-- ③配置项目访问白名单功能 begin-->
<!--<bean id="whiteListCheckAspect"-->
<!--class="pers.roamer.boracay.aspect.whitelist.WhiteListCheckAspect"></bean>-->
<!--<aop:config>-->
<!--<aop:pointcut-->
<!--id="whiteListCheckPointcut"-->
<!--expression="execution(* com.ninelephas.raccoon.controller..*.*(..))"/>-->
<!--<aop:aspect ref="whiteListCheckAspect">-->
<!--<aop:before method="whiteListCheck"-->
<!--pointcut-ref="whiteListCheckPointcut"/>-->
<!--</aop:aspect>-->
<!--</aop:config>-->
<!-- ③配置项目访问白名单功能 end-->
<!-- ④配置项目中需要进行短信验证码验证的功能 begin-->
<bean id="smsValidateCodeAspect"
class="pers.roamer.boracay.aspect.sms.SMSValidateCodeAspect"></bean>
<aop:config>
<aop:pointcut
id="smsValidateCodePointcut"
expression="execution(* pers.roamer.boracay.websample.controller..*.*(..))"/>
<aop:aspect ref="smsValidateCodeAspect">
<aop:before method="smsValidateCodeCheck"
pointcut-ref="smsValidateCodePointcut"/>
</aop:aspect>
</aop:config>
<!-- ④配置项目中需要进行短信验证码验证的功能 end-->
<!-- ⑤配置项目中需要进行session check,确定是否登录的功能 begin-->
<bean id="sessionCheckKeywordAspect"
class="pers.roamer.boracay.aspect.httprequest.SessionCheckKeywordAspect"></bean>
<aop:config>
<aop:pointcut
id="sessionKeywordCheckPointcut"
expression="execution(* pers.roamer.boracay.websample.controller..*.*(..))"/>
<aop:aspect ref="sessionCheckKeywordAspect">
<aop:before method="sessionKeywordCheck"
pointcut-ref="sessionKeywordCheckPointcut"/>
</aop:aspect>
</aop:config>
<!-- ⑤配置项目中需要进行session check,确定是否登录的功能 end-->
<!-- 不使用aop 方式,使用工具类的方式。以便更灵活的调用-->
<!-- ⑥配置项目中需要启动自动保存上传文件的功能 -->
<!--<bean id="uploadFileAutoSaveAspect"-->
<!--class="pers.roamer.boracay.aspect.fileupload.FileAutoSaveAspect"></bean>-->
<!--<aop:config>-->
<!--<aop:pointcut-->
<!--id="autoSavePointcut"-->
<!--expression="execution(* pers.roamer.boracay.websample.controller..*.*(..))"/>-->
<!--<aop:aspect ref="uploadFileAutoSaveAspect">-->
<!--<aop:around method="saveFileAction"-->
<!--pointcut-ref="smsValidateCodePointcut"/>-->
<!--</aop:aspect>-->
<!--</aop:config>-->
<!-- ⑥配置项目中需要启动自动保存上传文件的功能 end -->
<!-- 配置徐泽宇的 boracay 框架要到的 aop end-->
</beans>
先开启注解
在启动类上添加下面的注解
@EnableQuartzCluster
创建定时任务数据表
drop table if exists qrtz_fired_triggers;
drop table if exists qrtz_paused_trigger_grps;
drop table if exists qrtz_scheduler_state;
drop table if exists qrtz_locks;
drop table if exists qrtz_simple_triggers;
drop table if exists qrtz_simprop_triggers;
drop table if exists qrtz_cron_triggers;
drop table if exists qrtz_blob_triggers;
drop table if exists qrtz_triggers;
drop table if exists qrtz_job_details;
drop table if exists qrtz_calendars;
create table qrtz_job_details(
sched_name varchar(120) not null,
job_name varchar(200) not null,
job_group varchar(200) not null,
description varchar(250) null,
job_class_name varchar(250) not null,
is_durable varchar(1) not null,
is_nonconcurrent varchar(1) not null,
is_update_data varchar(1) not null,
requests_recovery varchar(1) not null,
job_data blob null,
primary key (sched_name,job_name,job_group))
engine=innodb;
create table qrtz_triggers (
sched_name varchar(120) not null,
trigger_name varchar(200) not null,
trigger_group varchar(200) not null,
job_name varchar(200) not null,
job_group varchar(200) not null,
description varchar(250) null,
next_fire_time bigint(13) null,
prev_fire_time bigint(13) null,
priority integer null,
trigger_state varchar(16) not null,
trigger_type varchar(8) not null,
start_time bigint(13) not null,
end_time bigint(13) null,
calendar_name varchar(200) null,
misfire_instr smallint(2) null,
job_data blob null,
primary key (sched_name,trigger_name,trigger_group),
foreign key (sched_name,job_name,job_group)
references qrtz_job_details(sched_name,job_name,job_group))
engine=innodb;
create table qrtz_simple_triggers (
sched_name varchar(120) not null,
trigger_name varchar(200) not null,
trigger_group varchar(200) not null,
repeat_count bigint(7) not null,
repeat_interval bigint(12) not null,
times_triggered bigint(10) not null,
primary key (sched_name,trigger_name,trigger_group),
foreign key (sched_name,trigger_name,trigger_group)
references qrtz_triggers(sched_name,trigger_name,trigger_group))
engine=innodb;
create table qrtz_cron_triggers (
sched_name varchar(120) not null,
trigger_name varchar(200) not null,
trigger_group varchar(200) not null,
cron_expression varchar(120) not null,
time_zone_id varchar(80),
primary key (sched_name,trigger_name,trigger_group),
foreign key (sched_name,trigger_name,trigger_group)
references qrtz_triggers(sched_name,trigger_name,trigger_group))
engine=innodb;
create table qrtz_simprop_triggers
(
sched_name varchar(120) not null,
trigger_name varchar(200) not null,
trigger_group varchar(200) not null,
str_prop_1 varchar(512) null,
str_prop_2 varchar(512) null,
str_prop_3 varchar(512) null,
int_prop_1 int null,
int_prop_2 int null,
long_prop_1 bigint null,
long_prop_2 bigint null,
dec_prop_1 numeric(13,4) null,
dec_prop_2 numeric(13,4) null,
bool_prop_1 varchar(1) null,
bool_prop_2 varchar(1) null,
primary key (sched_name,trigger_name,trigger_group),
foreign key (sched_name,trigger_name,trigger_group)
references qrtz_triggers(sched_name,trigger_name,trigger_group))
engine=innodb;
create table qrtz_blob_triggers (
sched_name varchar(120) not null,
trigger_name varchar(200) not null,
trigger_group varchar(200) not null,
blob_data blob null,
primary key (sched_name,trigger_name,trigger_group),
index (sched_name,trigger_name, trigger_group),
foreign key (sched_name,trigger_name,trigger_group)
references qrtz_triggers(sched_name,trigger_name,trigger_group))
engine=innodb;
create table qrtz_calendars (
sched_name varchar(120) not null,
calendar_name varchar(200) not null,
calendar blob not null,
primary key (sched_name,calendar_name))
engine=innodb;
create table qrtz_paused_trigger_grps (
sched_name varchar(120) not null,
trigger_group varchar(200) not null,
primary key (sched_name,trigger_group))
engine=innodb;
create table qrtz_fired_triggers (
sched_name varchar(120) not null,
entry_id varchar(95) not null,
trigger_name varchar(200) not null,
trigger_group varchar(200) not null,
instance_name varchar(200) not null,
fired_time bigint(13) not null,
sched_time bigint(13) not null,
priority integer not null,
state varchar(16) not null,
job_name varchar(200) null,
job_group varchar(200) null,
is_nonconcurrent varchar(1) null,
requests_recovery varchar(1) null,
primary key (sched_name,entry_id))
engine=innodb;
create table qrtz_scheduler_state (
sched_name varchar(120) not null,
instance_name varchar(200) not null,
last_checkin_time bigint(13) not null,
checkin_interval bigint(13) not null,
primary key (sched_name,instance_name))
engine=innodb;
create table qrtz_locks (
sched_name varchar(120) not null,
lock_name varchar(40) not null,
primary key (sched_name,lock_name))
engine=innodb;
create index idx_qrtz_j_req_recovery on qrtz_job_details(sched_name,requests_recovery);
create index idx_qrtz_j_grp on qrtz_job_details(sched_name,job_group);
create index idx_qrtz_t_j on qrtz_triggers(sched_name,job_name,job_group);
create index idx_qrtz_t_jg on qrtz_triggers(sched_name,job_group);
create index idx_qrtz_t_c on qrtz_triggers(sched_name,calendar_name);
create index idx_qrtz_t_g on qrtz_triggers(sched_name,trigger_group);
create index idx_qrtz_t_state on qrtz_triggers(sched_name,trigger_state);
create index idx_qrtz_t_n_state on qrtz_triggers(sched_name,trigger_name,trigger_group,trigger_state);
create index idx_qrtz_t_n_g_state on qrtz_triggers(sched_name,trigger_group,trigger_state);
create index idx_qrtz_t_next_fire_time on qrtz_triggers(sched_name,next_fire_time);
create index idx_qrtz_t_nft_st on qrtz_triggers(sched_name,trigger_state,next_fire_time);
create index idx_qrtz_t_nft_misfire on qrtz_triggers(sched_name,misfire_instr,next_fire_time);
create index idx_qrtz_t_nft_st_misfire on qrtz_triggers(sched_name,misfire_instr,next_fire_time,trigger_state);
create index idx_qrtz_t_nft_st_misfire_grp on qrtz_triggers(sched_name,misfire_instr,next_fire_time,trigger_group,trigger_state);
create index idx_qrtz_ft_trig_inst_name on qrtz_fired_triggers(sched_name,instance_name);
create index idx_qrtz_ft_inst_job_req_rcvry on qrtz_fired_triggers(sched_name,instance_name,requests_recovery);
create index idx_qrtz_ft_j_g on qrtz_fired_triggers(sched_name,job_name,job_group);
create index idx_qrtz_ft_jg on qrtz_fired_triggers(sched_name,job_group);
create index idx_qrtz_ft_t_g on qrtz_fired_triggers(sched_name,trigger_name,trigger_group);
create index idx_qrtz_ft_tg on qrtz_fired_triggers(sched_name,trigger_group);
commit;
在spring配置中添加quartz相关配置
spring:
quartz:
#相关属性配置
properties:
org:
quartz:
scheduler:
instanceName: clusteredScheduler
instanceId: AUTO
jobStore:
class: org.quartz.impl.jdbcjobstore.JobStoreTX
driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate
tablePrefix: qrtz_
isClustered: true
clusterCheckinInterval: 10000
useProperties: false
threadPool:
class: org.quartz.simpl.SimpleThreadPool
threadCount: 10
threadPriority: 5
threadsInheritContextClassLoaderOfInitializingThread: true
#数据库方式
job-store-type: jdbc
自定义Controller使用IScheduleService 例如:
@RestController
@RequestMapping("/job")
@Slf4j
public class ScheduleController {
@Autowired private IScheduleService scheduleService;
/**
* 添加cron定时任务
*
* @param cronJobDefinition
*/
@PostMapping("/addCronJob")
public void schedule(@RequestPart("cronJobDefinition") CronJobDefinition cronJobDefinition) {
scheduleService.schedule(cronJobDefinition);
}
/**
* 添加simple定时任务
*
* @param simpleJobDefinition
*/
@PostMapping("/addSimpleJob")
public void schedule(
@RequestPart("simpleJobDefinition") SimpleJobDefinition simpleJobDefinition) {
scheduleService.schedule(simpleJobDefinition);
}
}
如果有需要的,可以自定义SchedulerListener,JobListener和TriggerListener,并且交由spring来管理
@Service
public class ScheduleListenerService extends SchedulerListenerSupport {
//用来监听任务的执行状态
}
@Service
public class JobListenerService extends JobListenerSupport {
//用来监听任务的执行状态
}
@Service
public class TriggerListenerService extends TriggerListenerSupport {
//用来监听触发器的执行状态
}
1.下面来直接看看怎么和springboot集成:
//开启SimpleHttp
@EnableSimpleHttp
@SpringBootApplication
public class SimpleHttpApplication {
public static void main(String[] args) {
SpringApplication.run(SimpleHttpApplication.class, args);
}
}
开启SimpleHttp后,我们就先来使用它访问一下http://www.baidu.com试试:
@SimpleHttpService
public interface SimpleHttp {
/**
* 发送get请求访问http://www.baidu.com
* @return
*/
@Get("http://www.baidu.com")
String baidu();
}
接下来,我们就可以调用这个接口使用了:
@Service
public class TestService {
// 在spring中使用SimpleHttp
@Autowired private SimpleHttp simpleHttp;
public String baidu() {
return simpleHttp.baidu();
}
}
看到这里,就应该很明白了,我们的重点就在于如何编写SimpleHttp接口(ps:接口名称可以自己随便取)。
//如果想要url作为参数传入
@Get
String list(@Url String url);
//传入一个参数为id的字段
@Get
String find(@Url String url, @Field("id") Integer id);
//需要设置请求头的
@Get
String find(@Url String url, @Field("id") Integer id, @Header("Content-Type") String contentType);
//多个参数和请求头设置
@Get
String find(@Url String url, @Field Map<String, Object> params, @Header Map<String, String> headers);
如果是自定义对象:
//@Data是lombok的注解,可以避免手写get,set方法
// 请求参数实体类
@Data
public class PageParam {
private String search;
private int page;
private int size;
}
// 请求头实体类
@Data
public class RequestHeader {
private String cookie;
private String userAgent;
private String host;
}
@Get
String find(@Url String url, @Field PageParam params, @Header RequestHeader headers);
如果实体类中字段名称和实际请求名称不同:
// 请求参数实体类
@Data
public class PageParam {
@Field("name")
private String search;
private int page;
private int size;
}
// 请求头实体类
@Data
public class RequestHeader {
private String cookie;
@Header("User-Agent")
private String userAgent;
private String host;
}
你甚至还可以这样写:
@Data
public class PageParamAndRequestHeader {
@Field("name")
private String search;
@Field
private int page;
@Field
private int size;
@Header
private String cookie;
@Header("User-Agent")
private String userAgent;
@Header
private String host;
}
// 至于下面方法的参数修饰到底是使用@Field 还是@Header ,那就看你的心情了
@Get
String find(@Url String url, @Field PageParamAndRequestHeader paramsAndHeaders);
当然,除了Get请求,还有Post,Put,Delete,Patch......还有WebSocket
上传一个文件并且带参数:
// 带一个文件上传的,对应spring mvc里面的
// public User add(@RequestPart("user") User user, @RequestPart("image") MultipartFile[] image)
@Post
String add(@Url String url, @Field("user") Map<String, Object> user, @Field("image") File file);
// 和上面接口一样,只不过把Map改成了自定义的User
@Post
String add(@Url String url, @Field("user") User user, @Field("image") File file);
如果一个文件不够的话:
// 对应的服务端接口:
// public User add(@RequestPart("user") User user, @RequestPart("image1") MultipartFile[] image1, @RequestPart("image2") MultipartFile[] image2)
@Post
String add(@Url String url, @Field("user") User user, @Field("image1") File[] file, @Field("image2") File[] file);
以上接口,除文件外,参数也是一样可以多个的,使用方法同文件类似。 如果不需要上传文件,并且服务端是@RequestBody:
// 默认是对应服务端的@RequestBody,且格式化方式是JSON
// 如果发现有File或File[]参数,会自动对应@RequestPart,且携带的其他参数格式化方式是JSON
// 对应服务端接口
// public User add(@RequestBody User user)
@Post
String add(@Url String url, @Field User user);
// 同上
@Post
String add(@Url String url, @Field Map<String,Object> user);
如果需要监听文件上传进度怎么办?
// 添加ProgressListener上传进度监听器
@Post
String add(
@Field("user") User user,
@Field("image1") File[] file1,
@Field("image2") File[] file2, ProgressListener progressListener);
只要在参数中传一个进度监听器对象,即可获取当前进度,例如:
ProgressListener progressListener = (totalLength, currentLength) -> {
int progress = (int) (100 * currentLength / totalLength);
System.out.println(progress + "%");
}
当然,只有文件上传会有进度条。 你或许还需要发送异步请求:
import okhttp3.Callback;
// 添加异步回调Callback对象,这个时候方法就不需要返回值了
@Post
void add(@Url String url, @Field User user, Callback callback);
例如:
Callback callback = new Callback() {
@Override
public void onFailure(Call call, IOException e) {}
@Override
public void onResponse(Call call, Response response) throws IOException {
if (response.isSuccessful()) {
String result = response.body().string();
System.out.println(result);
}
}
})
很大一种可能,你发送的请求要求的数据格式不是JSON,可能是XML或是别的数据格式
public interface RequestParamsHandler {
String JSON_UTF8 = "application/json;charset=utf-8";
/**
* 处理请求参数
*
* @param params
* @param files
*/
RequestBody handle(Map<String, Object> params, Map<String, File[]> files);
}
例如需要将请求参数转换为XML格式,我们只需要实现自定义的RequestParamsHandler:
public class XMLRequestParamsHandler implements RequestParamsHandler {
@Override
public RequestBody handle(Map<String, Object> params, Map<String, File[]> files) {
//实现将params转换成XML格式,并且返回一个RequestBody对象
return null;
}
}
@Post(handler = XMLRequestParamsHandler.class)
String add(@Url String url, @Field User user)
这样的话,我们就可以发送数据格式化要求是XML的请求了。 除了请求参数可以自定义处理,我们还会希望返回结果也能自定义 目前默认的返回结果可以是void,String,Response,这三种返回结果程序会自动处理,不需要任何特殊设置。如果需要除此以外的返回类型,需要自定义ResponseHandler
public interface ResponseHandler {
/**
* 处理响应对象
*
* @param response
* @param <T>
* @return
*/
<T> T handle(Response response);
}
public class MyResponseHandler implements ResponseHandler {
@Override
public User handle(Response response) {
//处理Response,将返回数据转换成指定对象
return new User();
}
}
@Post
User add(@Url String url, @Field User user, ResponseHandler responseHandler)
如果想要自定义OkHttpClient,可以实现BaseOkHttpClientFactory:
public abstract class BaseOkHttpClientFactory {
/** final是为了保持单例 */
private final OkHttpClient client;
public BaseOkHttpClientFactory() {
this.client = httpClient();
}
/**
* 留给子类实现
*
* @return
*/
protected abstract OkHttpClient httpClient();
/**
* 获取创建好的OkHttpClient
*
* @return
*/
public OkHttpClient okHttpClient() {
return this.client;
}
}
public class DefaultOkHttpClientFactory extends BaseOkHttpClientFactory {
@Override
protected OkHttpClient httpClient() {
return new OkHttpClient();
}
}
上面是组件默认的。为什么要自定义OkHttpClient,是为了实现例如Https或者请求代理。
public class HttpProxyOkHttpClientFactory extends BaseOkHttpClientFactory {
@Override
protected OkHttpClient httpClient() {
OkHttpClient client = new OkHttpClient();
// 实现请求代理
return client;
}
}
@Post(clientFactory = HttpProxyOkHttpClientFactory.class)
String add(@Url String url, @Field User user);
最后,还有WebSocket:
import okhttp3.WebSocketListener;
// 创建WebSocket,url是ws://echo.websocket.org
@Ws("ws://echo.websocket.org")
WebSocket newWebSocket(WebSocketListener listener);
// 自定义url创建WebSocket
@Ws
WebSocket newWebSocket(@Url String url, WebSocketListener listener);