后期更新内容移步到我的网站:www.upheart.top/javascript
Spring Session 就是使用 Spring 中的代理过滤器,将全部的 Session 操做拦截下来,自动的将数据 同步到 Redis 中,或者自动的从 Redis 中读取数据php
对于开发者来讲,全部关于 Session 同步的操做都是透明的,开发者使用 Spring Session,一旦配置完成后,具体的用法就像使用一个普通的 Session 同样html
首先咱们须要有一个 https 证书,咱们能够从各个云服务厂商处申请一个免费的,不过本身作实验没有必要这么麻烦,咱们能够直接借助 Java 自带的 JDK 管理工具 keytool 来生成一个免费的 https 证书前端
进入到 %JAVVA_HOME%\bin
目录下,执行以下命令生成一个数字证书:java
keytool -genkey -alias tomcathttps -keyalg RSA -keysize 2048 -keystore D:\javaboy.p12 -validity 365
复制代码
命令含义以下:web
命令执行完成后 ,咱们在 D 盘目录下会看到一个名为 javaboy.p12 的文件ajax
接下来咱们须要在项目中引入 httpsredis
将上面生成的 javaboy.p12 拷贝到 Spring Boot 项目的 resources 目录下。而后在 application.properties 中添加以下配置:算法
server.ssl.key-store=classpath:javaboy.p12
server.ssl.key-alias=tomcathttps
server.ssl.key-store-password=111111
复制代码
其中:spring
配置完成后,就能够启动 Spring Boot 项目了,使用https访问
请求转发
考虑到 Spring Boot 不支持同时启动 HTTP 和 HTTPS ,为了解决这个问题,咱们这里能够配置一个请求转发,当用户发起 HTTP 调用时,自动转发到 HTTPS 上
具体配置以下:
@Configuration
public class TomcatConfig {
@Bean
TomcatServletWebServerFactory tomcatServletWebServerFactory() {
TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory(){
@Override
protected void postProcessContext(Context context) {
SecurityConstraint constraint = new SecurityConstraint();
constraint.setUserConstraint("CONFIDENTIAL");
SecurityCollection collection = new SecurityCollection();
collection.addPattern("/*");
constraint.addCollection(collection);
context.addConstraint(constraint);
}
};
factory.addAdditionalTomcatConnectors(createTomcatConnector());
return factory;
}
private Connector createTomcatConnector() {
Connector connector = new
Connector("org.apache.coyote.http11.Http11NioProtocol");
connector.setScheme("http");
connector.setPort(8081);
connector.setSecure(false);
connector.setRedirectPort(8080);
return connector;
}
}
复制代码
在这里,咱们配置了 Http 的请求端口为 8081,全部来自 8081 的请求,将被自动重定向到 8080 这个 https 的端口上
如此以后,咱们再去访问 http 请求,就会自动重定向到 https
Spring Boot 中的热部署相信你们都用过吧,只须要添加 spring-boot-devtools
依赖就能够轻松实现热部署Spring Boot 中热部署最最关键的原理就是两个不一样的 classloader:
实现修改Thymleaf不须要重启服务器
<!-- 热部署 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
复制代码
在application.propeties中添加spring.thymeleaf.cache = false
复制代码
IDAE中的设置:
File –> Setting –> Build, Execution, Deployment –> Compiler –> check this Build project automatically
Help---->Find Action打开一个小窗口后输入Registry Find and check this option compiler.automake.allow.when.app.running
从新编译类文件:
当咱们修改了一个java类的时候,咱们只须要从新编译一下,SpringBoot的就会重启了。由于devtools会监听classpath下的文件变更,因此当java类从新编译的时候,devtools会监听到这个变化,而后就会自动从新启动SpringBoot。这个重启是很是快的一个过程。由于在SpringBoot中有两个类加载器,一个是加载工程外部资源的,如jar包,还有一个类加载器是用来加载本工程的class的。因此在重启SpringBoot的时候只加载本工程的class文件
监听文件夹的变化:
若是你不想从新编译java类的话,还有一种方式用来让SpringBoot重启,那就是让devtools监听文件夹的变化:好比咱们想让com.zkn.learnspringboot这个文件夹下的文件改变的时候,自动从新启动SpringBoot,那么咱们只要在application.properties中添加这样一句话就好了:spring.devtools.restart.additional-paths=com\zkn\learnspringboot
页面模板改变ctrl+F9能够从新编译当前页面并生效
# 热部署生效
spring.devtools.restart.enabled=true
# 关闭缓存,即时刷新,生产环境需改成true
#spring.freemarker.cache=false
spring.thymeleaf.cache=false
# 重启的目录,添加那个目录下的文件须要重启
spring.devtools.restart.additional-paths=boot-start/src/main/java
# classpath目录下的WEB-INF文件夹内容修改后不重启
#spring.devtools.restart.exclude=WEB-INF/**,static/**,public/**
复制代码
修改静态资源必定要重启项目才会生效吗?未必!
其中 base classloader 用来加载那些不会变化的类,例如各类第三方依赖,而 restart classloader 则用来加载那些会发生变化的类,例如你本身写的代码。Spring Boot 中热部署的原理就是当代码发生变化时,base classloader 不变,而 restart classloader 则会被废弃,被另外一个新的 restart classloader 代替。在整个过程当中,由于只从新加载了变化的类,因此启动速度要被重启快
可是有另一个问题,就是静态资源文件!使用 devtools ,默认状况下当静态资源发生变化时,并不会触发项目重启。虽然咱们能够经过配置解决这一问题,可是没有必要!由于静态资源文件发生变化后不须要编译,按理说保存后刷新下就能够访问到了
那么如何才能实现静态资源变化后,不编译就能自动刷新呢? LiveReload 能够帮助咱们实现这一功能!
devtools 中默认嵌入了 LiveReload 服务器,利用 LiveReload 能够实现静态文件的热部署,LiveReload 能够在资源发生变化时自动触发浏览器更新,LiveReload 支持 Chrome、Firefox 以及 Safari 。以 Chrome 为例,在 Chrome 应用商店搜索 LiveReload
在浏览器中打开项目的页面,而后点击浏览器右上角的 LiveReload 按钮,打开 LiveReload 链接
LiveReload 是和浏览器选项卡绑定在一块儿的,在哪一个选项卡中打开了 LiveReload,就在哪一个选项卡中访问页面,这样才有效果
打开 LiveReload 以后,咱们启动一个加了 devtools 依赖的 Spring Boot 项目:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
复制代码
此时随便在 resources/static 目录下添加一个静态 html 页面,而后启动 Spring Boot 项目,在打开了 LiveReload 的选项卡中访问 html 页面
访问成功后,咱们再去手动修改 html 页面代码,修改为功后,回到浏览器,不用作任何操做,就会发现浏览器自动刷新了,页面已经更新了
整个过程当中,个人 Spring Boot 项目并无重启
若是开发者安装而且启动了 LiveReload 插件,同时也添加了 devtools 依赖,可是却并不想当静态页面发生变化时浏览器自动刷新,那么能够在 application.properties 中添加以下代码进行配置:
spring.devtools.livereload.enabled=false
复制代码
建议开发者使用 LiveReload 策略而不是项目重启策略来实现静态资源的动态加载,由于项目重启所耗费时间通常来讲要超过使用LiveReload 所耗费的时间
Firefox 也能够安装 LiveReload 插件,装好以后和 Chrome 用法基本一致
在 Spring Boot 中,默认状况下,一共有5个位置能够放静态资源,五个路径分别是以下5个:
classpath:/META-INF/resources/
classpath:/resources/
classpath:/static/
classpath:/public/
/
复制代码
这里第5个 /
其实就是表示 webapp 目录中的静态资源也不被拦截。若是同一个文件分别出如今五个目录下,那么优先级也是按照上面列出的顺序。
不过,虽然有5个存储目录,除了第5个用的比较少以外,其余四个,系统默认建立了 classpath:/static/
, 正常状况下,咱们只须要将咱们的静态资源放到这个目录下便可,也不须要额外去建立其余静态资源目录,例如我在 classpath:/static/
目录下放了一张名为1.png 的图片,那么个人访问路径是:
http://localhost:8080/1.png
复制代码
固然,这个是系统默认配置,若是咱们并不想将资源放在系统默认的这五个位置上,也能够自定义静态资源位置和映射,自定义的方式也有两种,能够经过 application.properties 来定义,也能够在 Java 代码中来定义,下面分别来看。
application.properties
在配置文件中定义的方式比较简单,以下:
spring.resources.static-locations=classpath:/
spring.mvc.static-path-pattern=/**
复制代码
第一行配置表示定义资源位置,第二行配置表示定义请求 URL 规则。以上文的配置为例,若是咱们这样定义了,表示能够将静态资源放在 resources目录下的任意地方,咱们访问的时候固然也须要写完整的路径,例如在resources/static目录下有一张名为1.png 的图片,那么访问路径就是 http://localhost:8080/static/1.png
,注意此时的static不能省略。
Java 代码定义
固然,在Spring Boot中咱们也能够经过 Java代码来自定义,方式和 Java 配置的 SSM 比较相似,以下:
@Configuration
public class WebMVCConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/**").addResourceLocations("classpath:/aaa/");
}
}
复制代码
只须要在Spring Boot工程的/src/main/resources
目录下建立一个banner.txt
文件,而后将ASCII字符画复制进去,就能替换默认的banner了
一些属性设置:
${AnsiColor.BRIGHT_RED}
:设置控制台中输出内容的颜色${application.version}
:用来获取MANIFEST.MF
文件中的版本号${application.formatted-version}
:格式化后的${application.version}
版本信息${spring-boot.version}
:Spring Boot的版本号${spring-boot.formatted-version}
:格式化后的${spring-boot.version}
版本信息生成工具
咱们能够借助下面这些工具,轻松地根据文字或图片来生成用于Banner输出的字符画
同源策略
同源策略是由Netscape提出的一个著名的安全策略,它是浏览器最核心也最基本的安全功能,如今全部支持JavaScript的浏览器都会使用这个策略。所谓同源是指协议、域名以及端口要相同
同源策略是基于安全方面的考虑提出来的,这个策略自己没问题,可是咱们在实际开发中,因为各类缘由又常常有跨域的需求,传统的跨域方案是JSONP,JSONP虽然能解决跨域可是有一个很大的局限性,那就是只支持GET请求,不支持其余类型的请求,而今天咱们说的CORS(跨域源资源共享)(CORS,Cross-origin resource sharing)是一个W3C标准,它是一份浏览器技术的规范,提供了Web服务从不一样网域传来沙盒脚本的方法,以避开浏览器的同源策略,这是JSONP模式的现代版
在Spring框架中,对于CORS也提供了相应的解决方案,咱们就来看看SpringBoot中如何实现CORS
首先建立两个普通的SpringBoot项目,这个就不用我多说,第一个命名为provider提供服务,第二个命名为consumer消费服务,第一个配置端口为8080,第二个配置配置为8081,而后在provider上提供两个hello接口,一个get,一个post,以下:
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "hello";
}
@PostMapping("/hello")
public String hello2() {
return "post hello";
}
}
复制代码
在consumer的resources/static目录下建立一个html文件,发送一个简单的ajax请求,以下:
<div id="app"></div>
<input type="button" onclick="btnClick()" value="get_button">
<input type="button" onclick="btnClick2()" value="post_button">
<script> function btnClick() { $.get('http://localhost:8080/hello', function (msg) { $("#app").html(msg); }); } function btnClick2() { $.post('http://localhost:8080/hello', function (msg) { $("#app").html(msg); }); } </script>
复制代码
而后分别启动两个项目,发送请求按钮,观察浏览器控制台以下:
Access to XMLHttpRequest at 'http://localhost:8080/hello' from origin 'http://localhost:8081' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
复制代码
能够看到,因为同源策略的限制,请求没法发送成功
使用CORS能够在前端代码不作任何修改的状况下,实现跨域,那么接下来看看在provider中如何配置。首先能够经过@CrossOrigin注解配置某一个方法接受某一个域的请求,以下:
@RestController
public class HelloController {
@CrossOrigin(value = "http://localhost:8081")
@GetMapping("/hello")
public String hello() {
return "hello";
}
@CrossOrigin(value = "http://localhost:8081")
@PostMapping("/hello")
public String hello2() {
return "post hello";
}
}
复制代码
此时观察浏览器请求网络控制台,能够看到响应头中多了以下信息:
这个表示服务端愿意接收来自http://localhost:8081的请求,拿到这个信息后,浏览器就不会再去限制本次请求的跨域了
provider上,每个方法上都去加注解未免太麻烦了,在Spring Boot中,还能够经过全局配置一次性解决这个问题,全局配置只须要在配置类中重写addCorsMappings方法便可,以下:
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost:8081")
.allowedMethods("*")
.allowedHeaders("*");
}
}
复制代码
/**
表示本应用的全部方法都会去处理跨域请求,allowedMethods表示容许经过的请求数,allowedHeaders则表示容许的请求头。通过这样的配置以后,就没必要在每一个方法上单独配置跨域了
存在的问题
了解了整个CORS的工做过程以后,咱们经过Ajax发送跨域请求,虽然用户体验提升了,可是也有潜在的威胁存在,常见的就是CSRF(Cross-site request forgery)跨站请求伪造。跨站请求伪造也被称为one-click attack 或者 session riding,一般缩写为CSRF或者XSRF,是一种挟制用户在当前已登陆的Web应用程序上执行非本意的操做的攻击方法,举个例子:
假如一家银行用以运行转帐操做的URL地址以下:
http://icbc.com/aa?bb=cc
,那么,一个恶意攻击者能够在另外一个网站上放置以下代码:<img src="http://icbc.com/aa?bb=cc">
,若是用户访问了恶意站点,而她以前刚访问过银行不久,登陆信息还没有过时,那么她就会遭受损失。
基于此,浏览器在实际操做中,会对请求进行分类,分为简单请求,预先请求,带凭证的请求等,预先请求会首先发送一个options探测请求,和浏览器进行协商是否接受请求。默认状况下跨域请求是不须要凭证的,可是服务端能够配置要求客户端提供凭证,这样就能够有效避免csrf攻击
假设从 aaa@qq.com 发送邮件到 111@163.com :
SMTP 协议全称为 Simple Mail Transfer Protocol,译做简单邮件传输协议,它定义了邮件客户端软件与 SMTP 服务器之间,以及 SMTP 服务器与 SMTP 服务器之间的通讯规则
也就是说 aaa@qq.com 用户先将邮件投递到腾讯的 SMTP 服务器这个过程就使用了 SMTP 协议,而后腾讯的 SMTP 服务器将邮件投递到网易的 SMTP 服务器这个过程也依然使用了 SMTP 协议,SMTP 服务器就是用来收邮件
而 POP3 协议全称为 Post Office Protocol ,译做邮局协议,它定义了邮件客户端与 POP3 服务器之间的通讯规则,那么该协议在什么场景下会用到呢?当邮件到达网易的 SMTP 服务器以后, 111@163.com 用户须要登陆服务器查看邮件,这个时候就该协议就用上了:邮件服务商都会为每个用户提供专门的邮件存储空间,SMTP 服务器收到邮件以后,就将邮件保存到相应用户的邮件存储空间中,若是用户要读取邮件,就须要经过邮件服务商的 POP3 邮件服务器来完成
最后,可能也有小伙伴们据说过 IMAP 协议,这个协议是对 POP3 协议的扩展,功能更强,做用相似
目前国内大部分的邮件服务商都不容许直接使用用户名/密码的方式来在代码中发送邮件,都是要先申请受权码,这里以 QQ 邮箱为例,向你们演示受权码的申请流程:首先咱们须要先登陆 QQ 邮箱网页版,点击上方的设置按钮,而后点击帐户选项卡,在帐户选项卡中找到开启POP3/SMTP选项,点击开启,开启相关功能,开启过程须要手机号码验证,按照步骤操做便可,不赘述。开启成功以后,便可获取一个受权码,将该号码保存好,一会使用
接下来,咱们就能够建立项目了,Spring Boot 中,对于邮件发送提供了自动配置类,开发者只须要加入相关依赖,而后配置一下邮箱的基本信息,就能够发送邮件了
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
复制代码
项目建立成功后,接下来在 application.properties 中配置邮箱的基本信息:
spring.mail.host=smtp.qq.com
spring.mail.port=587
spring.mail.username=1510161612@qq.com
spring.mail.password=ubknfzhjkhrbbabe
spring.mail.default-encoding=UTF-8
spring.mail.properties.mail.smtp.socketFactoryClass=javax.net.ssl.SSLSocketFactory
spring.mail.properties.mail.debug=true
复制代码
配置含义分别以下:
若是不知道 smtp 服务器的端口或者地址的的话,能够参考 腾讯的邮箱文档
作完这些以后,Spring Boot 就会自动帮咱们配置好邮件发送类,相关的配置在 org.springframework.boot.autoconfigure.mail.MailSenderAutoConfiguration
类中,部分源码以下:
@Configuration
@ConditionalOnClass({ MimeMessage.class, MimeType.class, MailSender.class })
@ConditionalOnMissingBean(MailSender.class)
@Conditional(MailSenderCondition.class)
@EnableConfigurationProperties(MailProperties.class)
@Import({ MailSenderJndiConfiguration.class, MailSenderPropertiesConfiguration.class })
public class MailSenderAutoConfiguration {
}
复制代码
从这段代码中,能够看到,导入了另一个配置 MailSenderPropertiesConfiguration
类,这个类中,提供了邮件发送相关的工具类:
@Configuration
@ConditionalOnProperty(prefix = "spring.mail", name = "host")
class MailSenderPropertiesConfiguration {
private final MailProperties properties;
MailSenderPropertiesConfiguration(MailProperties properties) {
this.properties = properties;
}
@Bean
@ConditionalOnMissingBean
public JavaMailSenderImpl mailSender() {
JavaMailSenderImpl sender = new JavaMailSenderImpl();
applyProperties(sender);
return sender;
}
}
复制代码
能够看到,这里建立了一个 JavaMailSenderImpl
的实例, JavaMailSenderImpl
是 JavaMailSender
的一个实现,咱们将使用 JavaMailSenderImpl
来完成邮件的发送工做
作完如上两步,邮件发送的准备工做就算是完成了,接下来就能够直接发送邮件了
具体的发送,有 5 种不一样的方式,咱们一个一个来看
简单邮件就是指邮件内容是一个普通的文本文档:
@Autowired
JavaMailSender javaMailSender;
@Test
public void sendSimpleMail() {
SimpleMailMessage message = new SimpleMailMessage();
message.setSubject("这是一封测试邮件");
message.setFrom("1510161612@qq.com");
message.setTo("25xxxxx755@qq.com");
message.setCc("37xxxxx37@qq.com");
message.setBcc("14xxxxx098@qq.com");
message.setSentDate(new Date());
message.setText("这是测试邮件的正文");
javaMailSender.send(message);
}
复制代码
从上往下,代码含义分别以下:
邮件的附件能够是图片,也能够是普通文件,都是支持的
@Test
public void sendAttachFileMail() throws MessagingException {
MimeMessage mimeMessage = javaMailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(mimeMessage,true);
helper.setSubject("这是一封测试邮件");
helper.setFrom("1510161612@qq.com");
helper.setTo("25xxxxx755@qq.com");
helper.setCc("37xxxxx37@qq.com");
helper.setBcc("14xxxxx098@qq.com");
helper.setSentDate(new Date());
helper.setText("这是测试邮件的正文");
helper.addAttachment("javaboy.jpg",new File("C:\\Users\\sang\\Downloads\\javaboy.png"));
javaMailSender.send(mimeMessage);
}
复制代码
注意这里在构建邮件对象上和前文有所差别,这里是经过 javaMailSender 来获取一个复杂邮件对象,而后再利用 MimeMessageHelper 对邮件进行配置,MimeMessageHelper 是一个邮件配置的辅助工具类,建立时候的 true 表示构建一个 multipart message 类型的邮件,有了 MimeMessageHelper 以后,咱们针对邮件的配置都是由 MimeMessageHelper 来代劳
最后经过 addAttachment 方法来添加一个附件
图片资源和附件有什么区别呢?图片资源是放在邮件正文中的,即一打开邮件,就能看到图片。可是通常来讲,不建议使用这种方式,一些公司会对邮件内容的大小有限制(由于这种方式是将图片一块儿发送的)
@Test
public void sendImgResMail() throws MessagingException {
MimeMessage mimeMessage = javaMailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true);
helper.setSubject("这是一封测试邮件");
helper.setFrom("1510161612@qq.com");
helper.setTo("25xxxxx755@qq.com");
helper.setCc("37xxxxx37@qq.com");
helper.setBcc("14xxxxx098@qq.com");
helper.setSentDate(new Date());
helper.setText("<p>hello 你们好,这是一封测试邮件,这封邮件包含两种图片,分别以下</p><p>第一张图片:</p><img src='cid:p01'/><p>第二张图片:</p><img src='cid:p02'/>",true);
helper.addInline("p01",new FileSystemResource(new File("C:\\Users\\sang\\Downloads\\javaboy.png")));
helper.addInline("p02",new FileSystemResource(new File("C:\\Users\\sang\\Downloads\\javaboy2.png")));
javaMailSender.send(mimeMessage);
}
复制代码
这里的邮件 text 是一个 HTML 文本,里边涉及到的图片资源先用一个占位符占着,setText 方法的第二个参数 true 表示第一个参数是一个 HTML 文本
setText 以后,再经过 addInline 方法来添加图片资源
首先须要引入 Freemarker 依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
复制代码
而后在 resources/templates
目录下建立一个 mail.ftl
做为邮件发送模板:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<p>hello 欢迎加入 xxx 你们庭,您的入职信息以下:</p>
<table border="1">
<tr>
<td>姓名</td>
<td>${username}</td>
</tr>
<tr>
<td>工号</td>
<td>${num}</td>
</tr>
<tr>
<td>薪水</td>
<td>${salary}</td>
</tr>
</table>
<div style="color: #ff1a0e">一块儿努力创造辉煌</div>
</body>
</html>
复制代码
接下来,将邮件模板渲染成 HTML ,而后发送便可
@Test
public void sendFreemarkerMail() throws MessagingException, IOException, TemplateException {
MimeMessage mimeMessage = javaMailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true);
helper.setSubject("这是一封测试邮件");
helper.setFrom("1510161612@qq.com");
helper.setTo("25xxxxx755@qq.com");
helper.setCc("37xxxxx37@qq.com");
helper.setBcc("14xxxxx098@qq.com");
helper.setSentDate(new Date());
//构建 Freemarker 的基本配置
Configuration configuration = new Configuration(Configuration.VERSION_2_3_0);
// 配置模板位置
ClassLoader loader = MailApplication.class.getClassLoader();
configuration.setClassLoaderForTemplateLoading(loader, "templates");
//加载模板
Template template = configuration.getTemplate("mail.ftl");
User user = new User();
user.setUsername("javaboy");
user.setNum(1);
user.setSalary((double) 99999);
StringWriter out = new StringWriter();
//模板渲染,渲染的结果将被保存到 out 中 ,将out 中的 html 字符串发送便可
template.process(user, out);
helper.setText(out.toString(),true);
javaMailSender.send(mimeMessage);
}
复制代码
须要注意的是,虽然引入了 Freemarker
的自动化配置,可是咱们在这里是直接 new Configuration
来从新配置 Freemarker
的,因此 Freemarker 默认的配置这里不生效,所以,在填写模板位置时,值为 templates
推荐在 Spring Boot 中使用 Thymeleaf 来构建邮件模板。由于 Thymeleaf 的自动化配置提供了一个 TemplateEngine,经过 TemplateEngine 能够方便的将 Thymeleaf 模板渲染为 HTML ,同时,Thymeleaf 的自动化配置在这里是继续有效的
首先,引入 Thymeleaf 依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
复制代码
而后,建立 Thymeleaf
邮件模板:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<p>hello 欢迎加入 xxx 你们庭,您的入职信息以下:</p>
<table border="1">
<tr>
<td>姓名</td>
<td th:text="${username}"></td>
</tr>
<tr>
<td>工号</td>
<td th:text="${num}"></td>
</tr>
<tr>
<td>薪水</td>
<td th:text="${salary}"></td>
</tr>
</table>
<div style="color: #ff1a0e">一块儿努力创造辉煌</div>
</body>
</html>
复制代码
接下来发送邮件:
@Autowired
TemplateEngine templateEngine;
@Test
public void sendThymeleafMail() throws MessagingException {
MimeMessage mimeMessage = javaMailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true);
helper.setSubject("这是一封测试邮件");
helper.setFrom("1510161612@qq.com");
helper.setTo("25xxxxx755@qq.com");
helper.setCc("37xxxxx37@qq.com");
helper.setBcc("14xxxxx098@qq.com");
helper.setSentDate(new Date());
Context context = new Context();
context.setVariable("username", "javaboy");
context.setVariable("num","000001");
context.setVariable("salary", "99999");
String process = templateEngine.process("mail.html", context);
helper.setText(process,true);
javaMailSender.send(mimeMessage);
}
复制代码
Spring Boot 打包成的可执行 jar ,为何不能被其余项目依赖?
Spring Boot 中默认打包成的 jar 叫作 可执行 jar,这种 jar 不一样于普通的 jar,普通的 jar 不能够经过 java -jar xxx.jar
命令执行,普通的 jar
主要是被其余应用依赖,Spring Boot
打成的 jar
能够执行,可是不能够被其余的应用所依赖,即便强制依赖,也没法获取里边的类。可是可执行 jar 并非 Spring Boot 独有的,Java 工程自己就能够打包成可执行 jar
有的小伙伴可能就有疑问了,既然一样是执行 mvn package
命令进行项目打包,为何 Spring Boot 项目就打成了可执行 jar ,而普通项目则打包成了不可执行 jar 呢?
这咱们就不得不提 Spring Boot 项目中一个默认的插件配置 spring-boot-maven-plugin
这个打包插件存在 5 个方面的功能
五个功能分别是:
mvn package
执行以后,这个命令再次打包生成可执行的 jar,同时将 mvn package
生成的 jar 重命名为 *.origin
mvn integration-test
阶段,进行 Spring Boot
应用生命周期的管理mvn integration-test
阶段,进行 Spring Boot
应用生命周期的管理这里功能,默认状况下使用就是 repackage 功能,其余功能要使用,则须要开发者显式配置
repackage 功能的 做用,就是在打包的时候,多作一点额外的事情:
mvn package
命令 对项目进行打包,打成一个 jar
,这个 jar
就是一个普通的 jar
,能够被其余项目依赖,可是不能够被执行repackage
命令,对第一步 打包成的 jar
进行再次打包,将之打成一个 可执行 jar
,经过将第一步打成的 jar
重命名为 *.original
文件对任意一个 Spring Boot 项目进行打包,能够执行 mvn package
命令,也能够直接在 IDEA
中点击 package
打包成功以后, target
中的文件
这里有两个文件,第一个 restful-0.0.1-SNAPSHOT.jar
表示打包成的可执行 jar
,第二个 restful-0.0.1-SNAPSHOT.jar.original
则是在打包过程当中 ,被重命名的 jar
,这是一个不可执行 jar
,可是能够被其余项目依赖的 jar
。经过对这两个文件的解压,咱们能够看出这二者之间的差别
通常来讲,Spring Boot 直接打包成可执行 jar
就能够了,不建议将 Spring Boot 做为普通的 jar
被其余的项目所依赖。若是有这种需求,建议将被依赖的部分,单独抽出来作一个普通的 Maven
项目,而后在 Spring Boot 中引用这个 Maven
项目。
若是非要将 Spring Boot 打包成一个普通 jar
被其余项目依赖,技术上来讲,也是能够的,给 spring-boot-maven-plugin
插件添加以下配置:
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<classifier>exec</classifier>
</configuration>
</plugin>
</plugins>
</build>
复制代码
配置的 classifier
表示可执行 jar
的名字,配置了这个以后,在插件执行 repackage
命令时,就不会给 mvn package
所打成的 jar
重命名了
打包后的 jar 以下:restful-0.0.1-SNAPSHOT.jar和restful-0.0.1-SNAPSHOT-exec.jar
第一个 jar 表示能够被其余项目依赖的 jar ,第二个 jar 则表示一个可执行 jar
在Spring Boot中,咱们只须要经过使用@Async
注解就能简单的将原来的同步函数变为异步函数,Task类改在为以下模式:
@Component
public class Task {
@Async
public void doTaskOne() throws Exception {
// 同上内容,省略
}
@Async
public void doTaskTwo() throws Exception {
// 同上内容,省略
}
@Async
public void doTaskThree() throws Exception {
// 同上内容,省略
}
}
复制代码
为了让@Async注解可以生效,还须要在Spring Boot的主程序中配置@EnableAsync,以下所示:
@SpringBootApplication
@EnableAsync
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
复制代码
注: @Async所修饰的函数不要定义为static类型,这样异步调用不会生效
为了让doTaskOne
、doTaskTwo
、doTaskThree
能正常结束,假设咱们须要统计一下三个任务并发执行共耗时多少,这就须要等到上述三个函数都完成调动以后记录时间,并计算结果
那么咱们如何判断上述三个异步调用是否已经执行完成呢?咱们须要使用Future<T>
来返回异步调用的结果,就像以下方式改造doTaskOne
函数:
@Async
public Future<String> doTaskOne() throws Exception {
System.out.println("开始作任务一");
long start = System.currentTimeMillis();
Thread.sleep(random.nextInt(10000));
long end = System.currentTimeMillis();
System.out.println("完成任务一,耗时:" + (end - start) + "毫秒");
return new AsyncResult<>("任务一完成");
}
复制代码
按照如上方式改造一下其余两个异步函数以后,下面咱们改造一下测试用例,让测试在等待完成三个异步调用以后来作一些其余事情
@Test
public void test() throws Exception {
long start = System.currentTimeMillis();
Future<String> task1 = task.doTaskOne();
Future<String> task2 = task.doTaskTwo();
Future<String> task3 = task.doTaskThree();
while(true) {
if(task1.isDone() && task2.isDone() && task3.isDone()) {
// 三个任务都调用完成,退出循环等待
break;
}
Thread.sleep(1000);
}
long end = System.currentTimeMillis();
System.out.println("任务所有完成,总耗时:" + (end - start) + "毫秒");
}
复制代码
看看咱们作了哪些改变:
Future<String>
类型的结果对象Future<String>
对象来判断三个异步函数是否都结束了。若都结束,就结束循环;若没有都结束,就等1秒后再判断执行一下上述的单元测试,能够看到以下结果:
开始作任务一
开始作任务二
开始作任务三
完成任务三,耗时:37毫秒
完成任务二,耗时:3661毫秒
完成任务一,耗时:7149毫秒
任务所有完成,总耗时:8025毫秒
复制代码
能够看到,经过异步调用,让任务1、2、三并发执行,有效的减小了程序的总运行时间
定义线程池
第一步,先在Spring Boot主类中定义一个线程池,好比:
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
@EnableAsync
@Configuration
class TaskPoolConfig {
@Bean("taskExecutor")
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(200);
executor.setKeepAliveSeconds(60);
executor.setThreadNamePrefix("taskExecutor-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
return executor;
}
}
}
复制代码
上面咱们经过使用ThreadPoolTaskExecutor
建立了一个线程池,同时设置了如下这些参数:
CallerRunsPolicy
策略,当线程池没有处理能力的时候,该策略会直接在 execute 方法的调用线程中运行被拒绝的任务;若是执行程序已关闭,则会丢弃该任务使用线程池
在定义了线程池以后,咱们如何让异步调用的执行任务使用这个线程池中的资源来运行呢?方法很是简单,咱们只须要在@Async
注解中指定线程池名便可,好比:
@Slf4j
@Component
public class Task {
public static Random random = new Random();
@Async("taskExecutor")
public void doTaskOne() throws Exception {
log.info("开始作任务一");
long start = System.currentTimeMillis();
Thread.sleep(random.nextInt(10000));
long end = System.currentTimeMillis();
log.info("完成任务一,耗时:" + (end - start) + "毫秒");
}
@Async("taskExecutor")
public void doTaskTwo() throws Exception {
log.info("开始作任务二");
long start = System.currentTimeMillis();
Thread.sleep(random.nextInt(10000));
long end = System.currentTimeMillis();
log.info("完成任务二,耗时:" + (end - start) + "毫秒");
}
@Async("taskExecutor")
public void doTaskThree() throws Exception {
log.info("开始作任务三");
long start = System.currentTimeMillis();
Thread.sleep(random.nextInt(10000));
long end = System.currentTimeMillis();
log.info("完成任务三,耗时:" + (end - start) + "毫秒");
}
}
复制代码
单元测试
最后,咱们来写个单元测试来验证一下
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest
public class ApplicationTests {
@Autowired
private Task task;
@Test
public void test() throws Exception {
task.doTaskOne();
task.doTaskTwo();
task.doTaskThree();
Thread.currentThread().join();
}
}
复制代码
执行上面的单元测试,咱们能够在控制台中看到全部输出的线程名前都是以前咱们定义的线程池前缀名开始的,说明咱们使用线程池来执行异步任务的试验成功了!
2018-03-27 22:01:15.620 INFO 73703 --- [ taskExecutor-1] com.didispace.async.Task : 开始作任务一
2018-03-27 22:01:15.620 INFO 73703 --- [ taskExecutor-2] com.didispace.async.Task : 开始作任务二
2018-03-27 22:01:15.620 INFO 73703 --- [ taskExecutor-3] com.didispace.async.Task : 开始作任务三
2018-03-27 22:01:18.165 INFO 73703 --- [ taskExecutor-2] com.didispace.async.Task : 完成任务二,耗时:2545毫秒
2018-03-27 22:01:22.149 INFO 73703 --- [ taskExecutor-3] com.didispace.async.Task : 完成任务三,耗时:6529毫秒
2018-03-27 22:01:23.912 INFO 73703 --- [ taskExecutor-1] com.didispace.async.Task
复制代码
第一步:如前文同样,咱们定义一个ThreadPoolTaskScheduler
线程池:
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
@EnableAsync
@Configuration
class TaskPoolConfig {
@Bean("taskExecutor")
public Executor taskExecutor() {
ThreadPoolTaskScheduler executor = new ThreadPoolTaskScheduler();
executor.setPoolSize(20);
executor.setThreadNamePrefix("taskExecutor-");
return executor;
}
}
}
复制代码
第二步:改造以前的异步任务,让它依赖一个外部资源,好比:Redis
@Slf4j
@Component
public class Task {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Async("taskExecutor")
public void doTaskOne() throws Exception {
log.info("开始作任务一");
long start = System.currentTimeMillis();
log.info(stringRedisTemplate.randomKey());
long end = System.currentTimeMillis();
log.info("完成任务一,耗时:" + (end - start) + "毫秒");
}
@Async("taskExecutor")
public void doTaskTwo() throws Exception {
log.info("开始作任务二");
long start = System.currentTimeMillis();
log.info(stringRedisTemplate.randomKey());
long end = System.currentTimeMillis();
log.info("完成任务二,耗时:" + (end - start) + "毫秒");
}
@Async("taskExecutor")
public void doTaskThree() throws Exception {
log.info("开始作任务三");
long start = System.currentTimeMillis();
log.info(stringRedisTemplate.randomKey());
long end = System.currentTimeMillis();
log.info("完成任务三,耗时:" + (end - start) + "毫秒");
}
}
复制代码
第三步:修改单元测试,模拟高并发状况下ShutDown的状况:
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest
public class ApplicationTests {
@Autowired
private Task task;
@Test
@SneakyThrows
public void test() {
for (int i = 0; i < 10000; i++) {
task.doTaskOne();
task.doTaskTwo();
task.doTaskThree();
if (i == 9999) {
System.exit(0);
}
}
}
}
复制代码
说明:经过for循环往上面定义的线程池中提交任务,因为是异步执行,在执行过程当中,利用System.exit(0)
来关闭程序,此时因为有任务在执行,就能够观察这些异步任务的销毁与Spring容器中其余资源的顺序是否安全
第四步:运行上面的单元测试,咱们将碰到下面的异常内容
org.springframework.data.redis.RedisConnectionFailureException: Cannot get Jedis connection; nested exception is redis.clients.jedis.exceptions.JedisConnectionException: Could not get a resource from the pool
at org.springframework.data.redis.connection.jedis.JedisConnectionFactory.fetchJedisConnector(JedisConnectionFactory.java:204) ~[spring-data-redis-1.8.10.RELEASE.jar:na]
at org.springframework.data.redis.connection.jedis.JedisConnectionFactory.getConnection(JedisConnectionFactory.java:348) ~[spring-data-redis-1.8.10.RELEASE.jar:na]
at org.springframework.data.redis.core.RedisConnectionUtils.doGetConnection(RedisConnectionUtils.java:129) ~[spring-data-redis-1.8.10.RELEASE.jar:na]
at org.springframework.data.redis.core.RedisConnectionUtils.getConnection(RedisConnectionUtils.java:92) ~[spring-data-redis-1.8.10.RELEASE.jar:na]
at org.springframework.data.redis.core.RedisConnectionUtils.getConnection(RedisConnectionUtils.java:79) ~[spring-data-redis-1.8.10.RELEASE.jar:na]
at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:194) ~[spring-data-redis-1.8.10.RELEASE.jar:na]
at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:169) ~[spring-data-redis-1.8.10.RELEASE.jar:na]
at org.springframework.data.redis.core.RedisTemplate.randomKey(RedisTemplate.java:781) ~[spring-data-redis-1.8.10.RELEASE.jar:na]
at com.didispace.async.Task.doTaskOne(Task.java:26) ~[classes/:na]
at com.didispace.async.Task$$FastClassBySpringCGLIB$$ca3ff9d6.invoke(<generated>) ~[classes/:na]
at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204) ~[spring-core-4.3.14.RELEASE.jar:4.3.14.RELEASE]
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:738) ~[spring-aop-4.3.14.RELEASE.jar:4.3.14.RELEASE]
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157) ~[spring-aop-4.3.14.RELEASE.jar:4.3.14.RELEASE]
at org.springframework.aop.interceptor.AsyncExecutionInterceptor$1.call(AsyncExecutionInterceptor.java:115) ~[spring-aop-4.3.14.RELEASE.jar:4.3.14.RELEASE]
at java.util.concurrent.FutureTask.run(FutureTask.java:266) [na:1.8.0_151]
at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$201(ScheduledThreadPoolExecutor.java:180) [na:1.8.0_151]
at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:293) [na:1.8.0_151]
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) [na:1.8.0_151]
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) [na:1.8.0_151]
at java.lang.Thread.run(Thread.java:748) [na:1.8.0_151]
Caused by: redis.clients.jedis.exceptions.JedisConnectionException: Could not get a resource from the pool
at redis.clients.util.Pool.getResource(Pool.java:53) ~[jedis-2.9.0.jar:na]
at redis.clients.jedis.JedisPool.getResource(JedisPool.java:226) ~[jedis-2.9.0.jar:na]
at redis.clients.jedis.JedisPool.getResource(JedisPool.java:16) ~[jedis-2.9.0.jar:na]
at org.springframework.data.redis.connection.jedis.JedisConnectionFactory.fetchJedisConnector(JedisConnectionFactory.java:194) ~[spring-data-redis-1.8.10.RELEASE.jar:na]
... 19 common frames omitted
Caused by: java.lang.InterruptedException: null
at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.reportInterruptAfterWait(AbstractQueuedSynchronizer.java:2014) ~[na:1.8.0_151]
at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awaitNanos(AbstractQueuedSynchronizer.java:2088) ~[na:1.8.0_151]
at org.apache.commons.pool2.impl.LinkedBlockingDeque.pollFirst(LinkedBlockingDeque.java:635) ~[commons-pool2-2.4.3.jar:2.4.3]
at org.apache.commons.pool2.impl.GenericObjectPool.borrowObject(GenericObjectPool.java:442) ~[commons-pool2-2.4.3.jar:2.4.3]
at org.apache.commons.pool2.impl.GenericObjectPool.borrowObject(GenericObjectPool.java:361) ~[commons-pool2-2.4.3.jar:2.4.3]
at redis.clients.util.Pool.getResource(Pool.java:49) ~[jedis-2.9.0.jar:na]
... 22 common frames omitted
复制代码
缘由分析
从异常信息JedisConnectionException: Could not get a resource from the pool
来看,咱们很容易的能够想到,在应用关闭的时候异步任务还在执行,因为Redis链接池先销毁了,致使异步任务中要访问Redis的操做就报了上面的错。因此,咱们得出结论,上面的实现方式在应用关闭的时候是不优雅的,那么咱们要怎么作呢?
解决方法
要解决上面的问题很简单,Spring的ThreadPoolTaskScheduler
为咱们提供了相关的配置,只须要加入以下设置便可:
@Bean("taskExecutor")
public Executor taskExecutor() {
ThreadPoolTaskScheduler executor = new ThreadPoolTaskScheduler();
executor.setPoolSize(20);
executor.setThreadNamePrefix("taskExecutor-");
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(60);
return executor;
}
复制代码
说明:setWaitForTasksToCompleteOnShutdown(true)
该方法就是这里的关键,用来设置线程池关闭的时候等待全部任务都完成再继续销毁其余的Bean,这样这些异步任务的销毁就会先于Redis线程池的销毁。同时,这里还设置了setAwaitTerminationSeconds(60)
,该方法用来设置线程池中任务的等待时间,若是超过这个时候尚未销毁就强制销毁,以确保应用最后可以被关闭,而不是阻塞住
定义异步任务
首先,咱们先使用@Async
注解来定义一个异步任务,这个方法返回Future
类型,具体以下:
@Slf4j
@Component
public class Task {
public static Random random = new Random();
@Async("taskExecutor")
public Future<String> run() throws Exception {
long sleep = random.nextInt(10000);
log.info("开始任务,需耗时:" + sleep + "毫秒");
Thread.sleep(sleep);
log.info("完成任务");
return new AsyncResult<>("test");
}
}
复制代码
Tips:什么是Future类型?
Future
是对于具体的Runnable
或者Callable
任务的执行结果进行取消、查询是否完成、获取结果的接口。必要时能够经过get方法获取执行结果,该方法会阻塞直到任务返回结果
它的接口定义以下:
public interface Future<V> {
boolean cancel(boolean mayInterruptIfRunning);
boolean isCancelled();
boolean isDone();
V get() throws InterruptedException, ExecutionException;
V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
}
复制代码
它声明这样的五个方法:
也就是说Future提供了三种功能:
测试执行与定义超时
在完成了返回Future
的异步任务定义以后,咱们来尝试实现一个单元测试来使用这个Future完成任务的执行,好比:
@Slf4j
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest
public class ApplicationTests {
@Autowired
private Task task;
@Test
public void test() throws Exception {
Future<String> futureResult = task.run();
String result = futureResult.get(5, TimeUnit.SECONDS);
log.info(result);
}
}
复制代码
上面的代码中,咱们在get方法中还定义了该线程执行的超时时间,经过执行这个测试咱们能够观察到执行时间超过5秒的时候,这里会抛出超时异常,该执行线程就可以因执行超时而释放回线程池,不至于一直阻塞而占用资源
消息转换器(Message Converter)
在Spring MVC中定义了HttpMessageConverter
接口,抽象了消息转换器对类型的判断、对读写的判断与操做,具体可见以下定义:
public interface HttpMessageConverter<T> {
boolean canRead(Class<?> clazz, @Nullable MediaType mediaType);
boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType);
List<MediaType> getSupportedMediaTypes();
T read(Class<? extends T> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException;
void write(T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException;
}
复制代码
众所周知,HTTP请求的Content-Type有各类不一样格式定义,若是要支持Xml格式的消息转换,就必需要使用对应的转换器。Spring MVC中默认已经有一套采用Jackson实现的转换器MappingJackson2XmlHttpMessageConverter
第一步:引入Xml消息转换器
在传统Spring应用中,咱们能够经过以下配置加入对Xml格式数据的消息转换实现:
@Configuration
public class MessageConverterConfig1 extends WebMvcConfigurerAdapter {
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.xml();
builder.indentOutput(true);
converters.add(new MappingJackson2XmlHttpMessageConverter(builder.build()));
}
}
复制代码
在Spring Boot应用不用像上面这么麻烦,只须要加入jackson-dataformat-xml
依赖,Spring Boot就会自动引入MappingJackson2XmlHttpMessageConverter
的实现:
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>
复制代码
同时,为了配置Xml数据与维护对象属性的关系所要使用的注解也在上述依赖中,因此这个依赖也是必须的
第二步:定义对象与Xml的关系
作好了基础扩展以后,下面就能够定义Xml内容对应的Java对象了,好比:
@Data
@NoArgsConstructor
@AllArgsConstructor
@JacksonXmlRootElement(localName = "User")
public class User {
@JacksonXmlProperty(localName = "name")
private String name;
@JacksonXmlProperty(localName = "age")
private Integer age;
}
复制代码
其中:@Data
、@NoArgsConstructor
、@AllArgsConstructor
是lombok简化代码的注解,主要用于生成get、set以及构造函数。@JacksonXmlRootElement
、@JacksonXmlProperty
注解是用来维护对象属性在xml中的对应关系
上述配置的User对象,其能够映射的Xml样例以下(后续可使用上述xml来请求接口):
<User>
<name>aaaa</name>
<age>10</age>
</User>
复制代码
第三步:建立接收xml请求的接口
完成了要转换的对象以后,能够编写一个接口来接收xml并返回xml,好比:
@Controller
public class UserController {
@PostMapping(value = "/user",
consumes = MediaType.APPLICATION_XML_VALUE,
produces = MediaType.APPLICATION_XML_VALUE)
@ResponseBody
public User create(@RequestBody User user) {
user.setName("didispace.com : " + user.getName());
user.setAge(user.getAge() + 100);
return user;
}
}
复制代码
Spring Boot提供了一个默认的映射:/error
,当处理中抛出异常以后,会转到该请求中处理,而且该请求有一个全局的错误页面用来展现异常内容
进行统一异常处理的改造
建立全局异常处理类:经过使用@ControllerAdvice
定义统一的异常处理类,而不是在每一个Controller中逐个定义。@ExceptionHandler
用来定义函数针对的异常类型,最后将Exception对象和请求URL映射到error.html
中
@ControllerAdvice
class GlobalExceptionHandler {
public static final String DEFAULT_ERROR_VIEW = "error";
@ExceptionHandler(value = Exception.class)
public ModelAndView defaultErrorHandler(HttpServletRequest req, Exception e) throws Exception {
ModelAndView mav = new ModelAndView();
mav.addObject("exception", e);
mav.addObject("url", req.getRequestURL());
mav.setViewName(DEFAULT_ERROR_VIEW);
return mav;
}
}
复制代码
实现error.html
页面展现:在templates
目录下建立error.html
,将请求的URL和Exception对象的message输出
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8" />
<title>统一异常处理</title>
</head>
<body>
<h1>Error Handler</h1>
<div th:text="${url}"></div>
<div th:text="${exception.message}"></div>
</body>
</html>
复制代码
咱们只须要在Controller中抛出Exception,固然咱们可能会有多种不一样的Exception。而后在@ControllerAdvice类中,根据抛出的具体Exception类型匹配@ExceptionHandler中配置的异常类型来匹配错误映射和处理
在上述例子中,经过@ControllerAdvice
统必定义不一样Exception映射到不一样错误处理页面。而当咱们要实现RESTful API时,返回的错误是JSON格式的数据,而不是HTML页面,这时候咱们也能轻松支持
本质上,只需在@ExceptionHandler
以后加入@ResponseBody
,就能让处理函数return的内容转换为JSON格式
下面以一个具体示例来实现返回JSON格式的异常处理
public class ErrorInfo<T> {
public static final Integer OK = 0;
public static final Integer ERROR = 100;
private Integer code;
private String message;
private String url;
private T data;
// 省略getter和setter
}
复制代码
public class MyException extends Exception {
public MyException(String message) {
super(message);
}
}
复制代码
Controller
中增长json映射,抛出MyException
异常@Controller
public class HelloController {
@RequestMapping("/json")
public String json() throws MyException {
throw new MyException("发生错误2");
}
}
复制代码
MyException
异常建立对应的处理@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(value = MyException.class)
@ResponseBody
public ErrorInfo<String> jsonErrorHandler(HttpServletRequest req, MyException e) throws Exception {
ErrorInfo<String> r = new ErrorInfo<>();
r.setMessage(e.getMessage());
r.setCode(ErrorInfo.ERROR);
r.setData("Some Data");
r.setUrl(req.getRequestURL().toString());
return r;
}
}
复制代码
{
code: 100,
data: "Some Data",
message: "发生错误2",
url: "http://localhost:8080/json"
}
复制代码
@EnableScheduling
注解,启用定时任务的配置@SpringBootApplication
@EnableScheduling
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
复制代码
@Component
public class ScheduledTasks {
private static final SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");
@Scheduled(fixedRate = 5000)
public void reportCurrentTime() {
System.out.println("如今时间:" + dateFormat.format(new Date()));
}
}
复制代码
@Scheduled详解
在上面的入门例子中,使用了@Scheduled(fixedRate = 5000)
注解来定义每过5秒执行的任务,对于@Scheduled
的使用能够总结以下几种方式:
@Scheduled(fixedRate = 5000)
:上一次开始执行时间点以后5秒再执行@Scheduled(fixedDelay = 5000)
:上一次执行完毕时间点以后5秒再执行@Scheduled(initialDelay=1000, fixedRate=5000)
:第一次延迟1秒后执行,以后按fixedRate的规则每5秒执行一次@Scheduled(cron="*/5 * * * * *")
:经过cron表达式定义规则将LocalDateTime字段以时间戳的方式返回给前端,添加日期转化类
public class LocalDateTimeConverter extends JsonSerializer<LocalDateTime> {
@Override
public void serialize(LocalDateTime value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
gen.writeNumber(value.toInstant(ZoneOffset.of("+8")).toEpochMilli());
}
}
复制代码
并在LocalDateTime字段上添加注解,以下:
@JsonSerialize(using = LocalDateTimeConverter.class)
protected LocalDateTime gmtModified;
复制代码
将LocalDateTime字段以指定格式化日期的方式返回给前端,在字段上添加注解便可,以下:
@JsonFormat(shape=JsonFormat.Shape.STRING, pattern="yyyy-MM-dd HH:mm:ss")
protected LocalDateTime gmtModified;
复制代码
对前端传入的日期进行格式化注解便可,以下:
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
protected LocalDateTime gmtModified;
复制代码
Spring Boot 在启动的时候会干这几件事情:
总结一下,其实就是 Spring Boot 在启动的时候,按照约定去读取 Spring Boot Starter 的配置信息,再根据配置信息对资源进行初始化,并注入到 Spring 容器中。这样 Spring Boot 启动完毕后,就已经准备好了一切资源,使用过程当中直接注入对应 Bean 资源便可