一、前言
上一篇《Spring Boot 入門之基礎篇(一)》介紹了 Spring Boot 的環境搭建以及項目啓動打包等基礎內容,本篇繼續深入介紹 Spring Boot 與 Web 開發相關的知識。
二、整合模板引擎
由於 jsp 不被 SpringBoot 推薦使用,所以模板引擎主要介紹 Freemarker 和 Thymeleaf。
# 2.1 整合 Freemarker
# 2.1.1 添加 Freemarker 依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
# 2.1.2 添加 Freemarker 模板配置
在 application.properties 中添加如下內容:
spring.freemarker.allow-request-override=false
spring.freemarker.cache=true
spring.freemarker.check-template-location=true
spring.freemarker.charset=UTF-8
spring.freemarker.content-type=text/html
spring.freemarker.expose-request-attributes=false
spring.freemarker.expose-session-attributes=false
spring.freemarker.expose-spring-macro-helpers=false
spring.freemarker.prefix=
spring.freemarker.suffix=.ftl
上述配置都是默認值。
# 2.1.3 Freemarker 案例演示
在 controller 包中創建 FreemarkerController:
@Controller
@RequestMapping("freemarker")
public class FreemarkerController {
@RequestMapping("hello")
public String hello(Map<String,Object> map) {
map.put("msg", "Hello Freemarker");
return "hello";
}
}
在 templates 目錄中創建名爲 hello.ftl 文件,內容如下:
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>Document</title>
<link href="/css/index.css" rel="stylesheet"/>
</head>
<body>
<div class="container">
<h2>${msg}</h2>
</div>
</body>
</html>
結果如下:
# 2.2 整合 Thymeleaf
# 2.2.1 添加 Thymeleaf 依賴
在 pom.xml 文件中添加:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
# 2.2.2 添加 Thymeleaf 模板配置
在 application.properties 中添加如下內容:
spring.thymeleaf.cache=true
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html
spring.thymeleaf.mode=HTML5
spring.thymeleaf.encoding=UTF-8
spring.thymeleaf.content-type=text/html
上述配置都是默認值。
# 2.2.3 Thymeleaf 案例演示
在 controller 包中創建 ThymeleafController:
@Controller
@RequestMapping("thymeleaf")
public class ThymeleafController {
@RequestMapping("hello")
public String hello(Map<String,Object> map) {
map.put("msg", "Hello Thymeleaf");
return "hello";
}
}
在 template 目錄下創建名爲 hello.html 的文件,內容如下:
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>Document</title>
<link href="/css/index.css" rel="stylesheet"/>
</head>
<body>
<div class="container">
<h2 th:text="${msg}"></h2>
</div>
</body>
</html>
結果如下:
三、整合 Fastjson
# 3.1 添加依賴
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.35</version>
</dependency>
# 3.2 整合 Fastjson
創建一個配置管理類 WebConfig ,如下:
@Configuration
public class WebConfig {
@Bean
public HttpMessageConverters fastJsonHttpMessageConverters() {
FastJsonHttpMessageConverter fastJsonHttpMessageConverter = new FastJsonHttpMessageConverter();
FastJsonConfig fastJsonConfig = new FastJsonConfig();
fastJsonConfig.setSerializerFeatures(SerializerFeature.PrettyFormat);
fastJsonHttpMessageConverter.setFastJsonConfig(fastJsonConfig);
HttpMessageConverter<?> converter = fastJsonHttpMessageConverter;
return new HttpMessageConverters(converter);
}
}
# 3.3 演示案例:
創建一個實體類 User:
public class User {
private Integer id;
private String username;
private String password;
private Date birthday;
}
getter 和 setter 此處省略。
創建控制器類 FastjsonController :
@Controller
@RequestMapping("fastjson")
public class FastJsonController {
@RequestMapping("/test")
@ResponseBody
public User test() {
User user = new User();
user.setId(1);
user.setUsername("jack");
user.setPassword("jack123");
user.setBirthday(new Date());
return user;
}
}
打開瀏覽器,訪問 http://localhost:8080/fastjson/test,結果如下圖:
此時,還不能看出 Fastjson 是否正常工作,我們在 User 類中使用 Fastjson 的註解,如下內容:
@JSONField(format="yyyy-MM-dd")
private Date birthday;
再次訪問 http://localhost:8080/fastjson/test,結果如下圖:
日期格式與我們修改的內容格式一致,說明 Fastjson 整合成功。
四、自定義 Servlet
# 4.1 編寫 Servlet
public class ServletTest extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doPost(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html;charset=utf-8");
resp.getWriter().write("自定義 Servlet");
}
}
# 4.2 註冊 Servlet
將 Servelt 註冊成 Bean。在上文創建的 WebConfig 類中添加如下代碼:
@Bean
public ServletRegistrationBean servletRegistrationBean() {
return new ServletRegistrationBean(new ServletTest(),"/servletTest");
}
結果如下:
五、自定義過濾器/第三方過濾器
# 5.1 編寫過濾器
public class TimeFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("=======初始化過濾器=========");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
throws IOException, ServletException {
long start = System.currentTimeMillis();
filterChain.doFilter(request, response);
System.out.println("filter 耗時:" + (System.currentTimeMillis() - start));
}
@Override
public void destroy() {
System.out.println("=======銷燬過濾器=========");
}
}
# 5.2 註冊過濾器
要是該過濾器生效,有兩種方式:
-
使用 @Component 註解
-
添加到過濾器鏈中,此方式適用於使用第三方的過濾器。將過濾器寫到 WebConfig 類中,如下:
@Bean
public FilterRegistrationBean timeFilter() {
FilterRegistrationBean registrationBean = new FilterRegistrationBean();
TimeFilter timeFilter = new TimeFilter();
registrationBean.setFilter(timeFilter);
List<String> urls = new ArrayList<>();
urls.add("/*");
registrationBean.setUrlPatterns(urls);
return registrationBean;
}
結果如下:
六、自定義監聽器
# 6.1 編寫監聽器
public class ListenerTest implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
System.out.println("監聽器初始化...");
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
}
}
# 6.2 註冊監聽器
註冊監聽器爲 Bean,在 WebConfig 配置類中添加如下代碼:
@Bean
public ServletListenerRegistrationBean<ListenerTest> servletListenerRegistrationBean() {
return new ServletListenerRegistrationBean<ListenerTest>(new ListenerTest());
}
當啓動容器時,結果如下:
針對自定義 Servlet、Filter 和 Listener 的配置,還有另一種方式:
@SpringBootApplication
public class SpringbootWebApplication implements ServletContextInitializer {
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
// 配置 Servlet
servletContext.addServlet("servletTest",new ServletTest())
.addMapping("/servletTest");
// 配置過濾器
servletContext.addFilter("timeFilter",new TimeFilter())
.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST),true,"/*");
// 配置監聽器
servletContext.addListener(new ListenerTest());
}
public static void main(String[] args) {
SpringApplication.run(SpringbootWebApplication.class, args);
}
}
七、自定義攔截器
# 7.1 編寫攔截器
使用 @Component 讓 Spring 管理其生命週期:
@Component
public class TimeInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("========preHandle=========");
System.out.println(((HandlerMethod)handler).getBean().getClass().getName());
System.out.println(((HandlerMethod)handler).getMethod().getName());
request.setAttribute("startTime", System.currentTimeMillis());
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)
throws Exception {
System.out.println("========postHandle=========");
Long start = (Long) request.getAttribute("startTime");
System.out.println("耗時:"+(System.currentTimeMillis() - start));
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception exception)
throws Exception {
System.out.println("========afterCompletion=========");
Long start = (Long) request.getAttribute("startTime");
System.out.println("耗時:"+(System.currentTimeMillis() - start));
System.out.println(exception);
}
}
# 7.2 註冊攔截器
編寫攔截器後,我們還需要將其註冊到攔截器鏈中,如下配置:
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter{
@Autowired
private TimeInterceptor timeInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(timeInterceptor);
}
}
請求一個 controller ,結果如下:
八、配置 AOP 切面
# 8.1 添加依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
# 8.2 編寫切面類
使用 @Component,@Aspect 標記到切面類上:
@Aspect
@Component
public class TimeAspect {
@Around("execution(* com.light.springboot.controller.FastJsonController..*(..))")
public Object method(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("=====Aspect處理=======");
Object[] args = pjp.getArgs();
for (Object arg : args) {
System.out.println("參數爲:" + arg);
}
long start = System.currentTimeMillis();
Object object = pjp.proceed();
System.out.println("Aspect 耗時:" + (System.currentTimeMillis() - start));
return object;
}
}
請求 FastJsonController 控制器的方法,結果如下:
九、錯誤處理
# 9.1 友好頁面
先演示非友好頁面,修改 FastJsonController 類中的 test 方法:
@RestController
@RequestMapping("fastjson")
public class FastJsonController {
@RequestMapping("/test")
public User test() {
User user = new User();
user.setId(1);
user.setUsername("jack");
user.setPassword("jack123");
user.setBirthday(new Date());
// 模擬異常
int i = 1/0;
return user;
}
}
瀏覽器請求:http://localhost:8080/fastjson/test,結果如下:
當系統報錯時,返回到頁面的內容通常是一些雜亂的代碼段,這種顯示對用戶來說不友好,因此我們需要自定義一個友好的提示系統異常的頁面。
在 src/main/resources 下創建 /public/error,在該目錄下再創建一個名爲 5xx.html 文件,該頁面的內容就是當系統報錯時返回給用戶瀏覽的內容:
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>系統錯誤</title>
<link href="/css/index.css" rel="stylesheet"/>
</head>
<body>
<div class="container">
<h2>系統內部錯誤</h2>
</div>
</body>
</html>
路徑時固定的,Spring Boot 會在系統報錯時將返回視圖指向該目錄下的文件。
如下圖:
上邊處理的 5xx 狀態碼的問題,接下來解決 404 狀態碼的問題。
當出現 404 的情況時,用戶瀏覽的頁面也不夠友好,因此我們也需要自定義一個友好的頁面給用戶展示。
在 /public/error 目錄下再創建一個名爲 404.html 的文件:
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>訪問異常</title>
<link href="/css/index.css" rel="stylesheet"/>
</head>
<body>
<div class="container">
<h2>找不到頁面</h2>
</div>
</body>
</html>
我們請求一個不存在的資源,如:http://localhost:8080/fastjson/test2,結果如下圖:
# 9.2 全局異常捕獲
如果項目前後端是通過 JSON 進行數據通信,則當出現異常時可以常用如下方式處理異常信息。
編寫一個類充當全局異常的處理類,需要使用 @ControllerAdvice 和 @ExceptionHandler 註解:
@ControllerAdvice
public class GlobalDefaultExceptionHandler {
/**
* 處理 Exception 類型的異常
* @param e
* @return
*/
@ExceptionHandler(Exception.class)
@ResponseBody
public Map<String,Object> defaultExceptionHandler(Exception e) {
Map<String,Object> map = new HashMap<String,Object>();
map.put("code", 500);
map.put("msg", e.getMessage());
return map;
}
}
其中,方法名爲任意名,入參一般使用 Exception 異常類,方法返回值可自定義。
啓動項目,訪問 http://localhost:8080/fastjson/test,結果如下圖:
我們還可以自定義異常,在全局異常的處理類中捕獲和判斷,從而對不同的異常做出不同的處理。
十、文件上傳和下載
# 10.1 添加依賴
<!-- 工具 -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.4</version>
</dependency>
# 10.2 實現
編寫一個實體類,用於封裝返回信息:
public class FileInfo {
private String path;
public FileInfo(String path) {
this.path = path;
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
}
編寫 Controller,用於處理文件上傳下載:
@RestController
@RequestMapping("/file")
public class FileController {
private String path = "d:\\";
@PostMapping
public FileInfo upload(MultipartFile file) throws Exception {
System.out.println(file.getName());
System.out.println(file.getOriginalFilename());
System.out.println(file.getSize());
File localFile = new File(path, file.getOriginalFilename());
file.transferTo(localFile);
return new FileInfo(localFile.getAbsolutePath());
}
@GetMapping("/{id}")
public void download(@PathVariable String id, HttpServletRequest request, HttpServletResponse response) {
try (InputStream inputStream = new FileInputStream(new File(path, id + ".jpg"));
OutputStream outputStream = response.getOutputStream();) {
response.setContentType("application/x-download");
response.addHeader("Content-Disposition", "attachment;filename=" + id + ".jpg");
IOUtils.copy(inputStream, outputStream);
} catch (Exception e) {
e.printStackTrace();
}
}
}
基本上都是在學習 javaweb 時用到的 API。
文件上傳測試結果如下圖:
十一、CORS 支持
前端頁面:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>跨域測試</title>
</head>
<body>
<button id="test">測試</button>
<script type="text/javascript" src="jquery-1.12.3.min.js"></script>
<script type="text/javascript">
$(function() {
$("#test").on("click", function() {
$.ajax({
"url": "http://localhost:8080/fastjson/test",
"type": "get",
"dataType": "json",
"success": function(data) {
console.log(data);
}
})
});
});
</script>
</body>
</html>
通過 http 容器啓動前端頁面代碼,筆者使用 Sublime Text 的插件啓動的,測試結果如下:
從圖中可知,前端服務器啓動端口爲 8088 與後端服務器 8080 不同源,因此出現跨域的問題。
現在開始解決跨域問題,可以兩種維度控制客戶端請求。
粗粒度控制:
方式一
@Configuration
public class WebConfig {
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurerAdapter() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/fastjson/**")
.allowedOrigins("http://localhost:8088");// 允許 8088 端口訪問
}
};
}
}
方式二
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter{
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/fastjson/**")
.allowedOrigins("http://localhost:8088");// 允許 8088 端口訪問
}
}
配置後,重新發送請求,結果如下:
細粒度控制:
在 FastJsonController 類中的方法上添加 @CrossOrigin(origins="xx") 註解:
@RequestMapping("/test")
@CrossOrigin(origins="http://localhost:8088")
public User test() {
User user = new User();
user.setId(1);
user.setUsername("jack");
user.setPassword("jack123");
user.setBirthday(new Date());
return user;
}
在使用該註解時,需要注意 @RequestMapping 使用的請求方式類型,即 GET 或 POST。
十二、整合 WebSocket
# 12.1 添加依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
# 12.2 實現方式
方式一:
該方式只適用於通過 jar 包直接運行項目的情況。
WebSocket 配置類:
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
WebSocket 處理類:
@ServerEndpoint(value = "/webSocketServer/{userName}")
@Component
public class WebSocketServer {
private static final Set<WebSocketServer> connections = new CopyOnWriteArraySet<>();
private String nickname;
private Session session;
private static String getDatetime(Date date) {
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return format.format(date);
}
@OnOpen
public void start(@PathParam("userName") String userName, Session session) {
this.nickname = userName;
this.session = session;
connections.add(this);
String message = String.format("* %s %s", nickname, "加入聊天!");
broadcast(message);
}
@OnClose
public void end() {
connections.remove(this);
String message = String.format("* %s %s", nickname, "退出聊天!");
broadcast(message);
}
@OnMessage
public void pushMsg(String message) {
broadcast("【" + this.nickname + "】" + getDatetime(new Date()) + " : " + message);
}
@OnError
public void onError(Throwable t) throws Throwable {
}
private static void broadcast(String msg) {
// 廣播形式發送消息
for (WebSocketServer client : connections) {
try {
synchronized (client) {
client.session.getBasicRemote().sendText(msg);
}
} catch (IOException e) {
connections.remove(client);
try {
client.session.close();
} catch (IOException e1) {
e.printStackTrace();
}
String message = String.format("* %s %s", client.nickname, "斷開連接");
broadcast(message);
}
}
}
}
前端頁面:
<!DOCTYPE html>
<html>
<head lang="zh">
<meta charset="UTF-8">
<link rel="stylesheet" href="css/bootstrap.min.css">
<link rel="stylesheet" href="css/bootstrap-theme.min.css">
<script src="js/jquery-1.12.3.min.js"></script>
<script src="js/bootstrap.js"></script>
<style type="text/css">
#msg {
height: 400px;
overflow-y: auto;
}
#userName {
width: 200px;
}
#logout {
display: none;
}
</style>
<title>webSocket測試</title>
</head>
<body>
<div class="container">
<div class="page-header" id="tou">webSocket及時聊天Demo程序</div>
<p class="text-right" id="logout">
<button class="btn btn-danger" id="logout-btn">退出</button>
</p>
<div class="well" id="msg"></div>
<div class="col-lg">
<div class="input-group">
<input type="text" class="form-control" placeholder="發送信息..." id="message"> <span class="input-group-btn">
<button class="btn btn-default" type="button" id="send"
disabled="disabled">發送</button>
</span>
</div>
<div class="input-group">
<input id="userName" type="text" class="form-control" name="userName" placeholder="輸入您的用戶名" />
<button class="btn btn-default" type="button" id="connection-btn">建立連接</button>
</div>
<!-- /input-group -->
</div>
<!-- /.col-lg-6 -->
</div>
<!-- /.row -->
</div>
<script type="text/javascript">
$(function() {
var websocket;
$("#connection-btn").bind("click", function() {
var userName = $("#userName").val();
if (userName == null || userName == "") {
alert("請輸入您的用戶名");
return;
}
connection(userName);
});
function connection(userName) {
var host = window.location.host;
if ('WebSocket' in window) {
websocket = new WebSocket("ws://" + host +
"/webSocketServer/" + userName);
} else if ('MozWebSocket' in window) {
websocket = new MozWebSocket("ws://" + host +
"/webSocketServer/" + userName);
}
websocket.onopen = function(evnt) {
$("#tou").html("鏈接服務器成功!")
$("#send").prop("disabled", "");
$("#connection-btn").prop("disabled", "disabled");
$("#logout").show();
};
websocket.onmessage = function(evnt) {
$("#msg").html($("#msg").html() + "<br/>" + evnt.data);
};
websocket.onerror = function(evnt) {
$("#tou").html("報錯!")
};
websocket.onclose = function(evnt) {
$("#tou").html("與服務器斷開了鏈接!");
$("#send").prop("disabled", "disabled");
$("#connection-btn").prop("disabled", "");
$("#logout").hide();
}
}
function send() {
if (websocket != null) {
var $message = $("#message");
var data = $message.val();
if (data == null || data == "") {
return;
}
websocket.send(data);
$message.val("");
} else {
alert('未與服務器鏈接.');
}
}
$('#send').bind('click', function() {
send();
});
$(document).on("keypress", function(event) {
if (event.keyCode == "13") {
send();
}
});
$("#logout-btn").on("click", function() {
websocket.close(); //關閉TCP連接
});
});
</script>
</body>
</html>
演示圖如下:
如果使用該方式實現 WebSocket 功能並打包成 war 運行會報錯:
javax.websocket.DeploymentException: Multiple Endpoints may not be deployed to the same path
方式二:
該方式適用於 jar 包方式運行和 war 方式運行。
WebSocket 配置類:
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(webSocketServer(), "/webSocketServer/*");
}
@Bean
public WebSocketHandler webSocketServer() {
return new WebSocketServer();
}
}
WebSocket 處理類:
public class WebSocketServer extends TextWebSocketHandler {
private static final Map<WebSocketSession, String> connections = new ConcurrentHashMap<>();
private static String getDatetime(Date date) {
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return format.format(date);
}
/**
* 建立連接
*/
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
String uri = session.getUri().toString();
String userName = uri.substring(uri.lastIndexOf("/") + 1);
String nickname = URLDecoder.decode(userName, "utf-8");
connections.put(session, nickname);
String message = String.format("* %s %s", nickname, "加入聊天!");
broadcast(new TextMessage(message));
}
/**
* 斷開連接
*/
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
String nickname = connections.remove(session);
String message = String.format("* %s %s", nickname, "退出聊天!");
broadcast(new TextMessage(message));
}
/**
* 處理消息
*/
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
String msg = "【" + connections.get(session) + "】" + getDatetime(new Date()) + " : " + message.getPayload();
broadcast(new TextMessage(msg));
}
private static void broadcast(TextMessage msg) {
// 廣播形式發送消息
for (WebSocketSession session : connections.keySet()) {
try {
synchronized (session) {
session.sendMessage(msg);
}
} catch (Exception e) {
connections.remove(session);
try {
session.close();
} catch (Exception e2) {
e2.printStackTrace();
}
String message = String.format("* %s %s", connections.get(session), "斷開連接");
broadcast(new TextMessage(message));
}
}
}
}
運行結果與上圖一致。
十三、整合 JavaMail
本次測試演示帶模板的郵件,使用 Freemark 實現郵件的模板。
# 13.1 添加依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
# 13.2 添加配置
在 application.properties 中添加
# javamail 配置
spring.mail.host=smtp.163.com
[email protected]
spring.mail.password=
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true
spring.mail.properties.mail.smtp.starttls.required=true
# 13.3 編碼
@Component
@EnableConfigurationProperties(MailProperties.class)
public class JavaMailComponent {
private static final String template = "mail.ftl";
@Autowired
private FreeMarkerConfigurer freeMarkerConfigurer;
@Autowired
private JavaMailSender javaMailSender;
@Autowired
private MailProperties mailProperties;
public void sendMail(String email) {
Map<String, Object> map = new HashMap<String, Object>();
map.put("email", email);
try {
// 獲取內容
String text = this.getTextByTemplate(template, map);
// 發送
this.send(email, text);
} catch (Exception e) {
e.printStackTrace();
}
}
private String getTextByTemplate(String template, Map<String, Object> model) throws Exception {
return FreeMarkerTemplateUtils
.processTemplateIntoString(this.freeMarkerConfigurer.getConfiguration().getTemplate(template), model);
}
private String send(String email, String text) throws MessagingException, UnsupportedEncodingException {
MimeMessage message = this.javaMailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
InternetAddress from = new InternetAddress();
from.setAddress(this.mailProperties.getUsername());
from.setPersonal("月光中的污點", "UTF-8");
helper.setFrom(from);
helper.setTo(email);
helper.setSubject("SpringBoot 發送的第一封郵件");
helper.setText(text, true);
this.javaMailSender.send(message);
return text;
}
}
在 src/main/resources 下的 template 目錄下創建名爲 mail.ftl 的文件,其內容如下:
<!DOCTYPE html>
<html lang="zh">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<div style="width: 600px; text-align: left; margin: 0 auto;">
<h1 style="color: #005da7;">月光中的污點</h1>
<div style="border-bottom: 5px solid #005da7; height: 2px; width: 100%;"></div>
<div style="border: 1px solid #005da7; font-size: 16px; line-height: 50px; padding: 20px;">
<div>${email},您好!</div>
<div>
這是個測試
</div>
<div>
想了解更多信息,請訪問 <a href="https://www.extlight.com">https://www.extlight.com</a>
</div>
</div>
</div>
</body>
</html>
# 13.4 測試
@RunWith(SpringRunner.class)
@SpringBootTest
public class MailTest {
@Autowired
private JavaMailComponent javaMailComponent;
@Test
public void test() {
this.javaMailComponent.sendMail("[email protected]");
}
}
運行結果如下圖:
十四、整合定時任務
定時器的實現有 2 種方式:
1) Scheduled:spring 3.0 後自帶的定時器
2)Quartz:第三放定時器框架
# 14.1 Scheduled 方式
# 14.1.1 任務類
@Component
public class Schedule {
@Scheduled(fixedRate = 2000)
public void task() {
System.out.println("啓動定時任務:" + new Date());
}
}
使用 @Scheduled 定義任務執行時間,代碼中表示每隔 2 秒執行一次任務。
# 14.1.2 開啓定時計劃
只需在 Spring Boot 的啓動類上添加 @EnableScheduling 後,啓動項目即可。
測試結果如下圖:
# 14.1.3 並行執行
默認情況下,Spring Task 使用一條線程串行的執行所有的定時任務。爲了提高執行效率,我們需要手動編寫一個線程池實現定時任務的並行執行。
@Configuration
@EnableScheduling
public class AsyncTaskConfig implements SchedulingConfigurer, AsyncConfigurer {
//線程池線程數量
private int corePoolSize = 5;
@Bean
public ThreadPoolTaskScheduler taskScheduler() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.initialize();//初始化線程池
scheduler.setPoolSize(corePoolSize);//線程池容量
return scheduler;
}
@Override
public Executor getAsyncExecutor() {
Executor executor = this.taskScheduler();
return executor;
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return null;
}
@Override
public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) {
scheduledTaskRegistrar.setTaskScheduler(taskScheduler());
}
}
只需編寫上邊的類即可實現並行的效果。
# 14.2 Quartz 方式
# 14.2.1 任務類
public class MyJob implements Job {
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
System.out.println("========quartz 測試==========");
}
}
# 14.2.2 配置類
@Configuration
public class QuartzConfiguration {
/**
* Job 工廠
* @return
*/
@Bean
public JobDetailFactoryBean jobDetailFactoryBean() {
JobDetailFactoryBean factory = new JobDetailFactoryBean();
factory.setJobClass(MyJob.class);
return factory;
}
/**
* Trigger 工廠
* @return
*/
@Bean
public SimpleTriggerFactoryBean simpleTriggerFactoryBean(JobDetailFactoryBean jobDetailFactory) {
SimpleTriggerFactoryBean factory = new SimpleTriggerFactoryBean();
factory.setJobDetail(jobDetailFactory.getObject());
// 執行間隔時間
factory.setRepeatInterval(5000);
// 重複執行次數
factory.setRepeatCount(3);
return factory;
}
/**
* Trigger 工廠
* @return
*/
@Bean
public CronTriggerFactoryBean cronTriggerFactoryBean(JobDetailFactoryBean jobDetailFactory) {
CronTriggerFactoryBean factory = new CronTriggerFactoryBean();
factory.setJobDetail(jobDetailFactory.getObject());
factory.setCronExpression("0/5 * * * * ?");
return factory;
}
/* @Bean
public SchedulerFactoryBean schedulerFactoryBean(SimpleTriggerFactoryBean simpleTriggerFactory){
SchedulerFactoryBean factory = new SchedulerFactoryBean();
factory.setTriggers(simpleTriggerFactory.getObject());
return factory;
}*/
@Bean
public SchedulerFactoryBean schedulerFactoryBean(CronTriggerFactoryBean cronTriggerFactory){
SchedulerFactoryBean factory = new SchedulerFactoryBean();
factory.setTriggers(cronTriggerFactory.getObject());
return factory;
}
}
同樣地,需要在 Spring Boot 的啓動類上添加 @EnableScheduling 後,啓動項目即可。
# 14.2.3 依賴注入問題
實際開發中,任務類需要注入業務組件來執行定時任務,如下:
public class MyJob implements Job {
@Autowired
private UserService userService;
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
this.userService.save();
}
}
但是,MyJob 生命週期並沒有被 Spring 容器管理,因此無法注入 UserService,當定時器執行任務時會報空指針異常。
解決方案:
自定義任務工廠,重寫創建任務實例的方法:
@Component("customAdaptableJobFactory")
public class CustomAdaptableJobFactory extends AdaptableJobFactory {
@Autowired
private AutowireCapableBeanFactory autowireCapableBeanFactory;
@Override
protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception {
Object object = super.createJobInstance(bundle);
// 將任務實例納入 Spring 容器中
this.autowireCapableBeanFactory.autowireBean(object);
return object;
}
}
修改 Scheduler 實現:
@Bean
public SchedulerFactoryBean schedulerFactoryBean(CronTriggerFactoryBean cronTriggerFactory,CustomAdaptableJobFactory customAdaptableJobFactory){
SchedulerFactoryBean factory = new SchedulerFactoryBean();
factory.setTriggers(cronTriggerFactory.getObject());
factory.setJobFactory(customAdaptableJobFactory);
return factory;
}
十五、整合 Swagger2
# 15.1 添加依賴
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.7.0</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.7.0</version>
</dependency>
# 15.2 配置
重新創建一個配置類,如下:
@Configuration
@EnableSwagger2
public class Swagger2Configuration {
@Bean
public Docket accessToken() {
return new Docket(DocumentationType.SWAGGER_2)
.groupName("api")// 定義組
.select() // 選擇那些路徑和 api 會生成 document
.apis(RequestHandlerSelectors.basePackage("com.light.springboot.controller")) // 攔截的包路徑
.paths(PathSelectors.regex("/*/.*"))// 攔截的接口路徑
.build() // 創建
.apiInfo(apiInfo()); // 配置說明
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()//
.title("Spring Boot 之 Web 篇")// 標題
.description("spring boot Web 相關內容")// 描述
.termsOfServiceUrl("http://www.extlight.com")//
.contact(new Contact("moonlightL", "http://www.extlight.com", "[email protected]"))// 聯繫
.version("1.0")// 版本
.build();
}
}
爲了能更好的說明接口信息,我們還可以在 Controller 類上使用 Swagger2 相關注解說明信息。
我們以 FastJsonController 爲例:
@Api(value = "FastJson測試", tags = { "測試接口" })
@RestController
@RequestMapping("fastjson")
public class FastJsonController {
@ApiOperation("獲取用戶信息")
@ApiImplicitParam(name = "name", value = "用戶名", dataType = "string", paramType = "query")
@GetMapping("/test/{name}")
public User test(@PathVariable("name") String name) {
User user = new User();
user.setId(1);
user.setUsername(name);
user.setPassword("jack123");
user.setBirthday(new Date());
return user;
}
}
注意,上邊的方法是用 @GetMapping 註解,如果只是使用 @RequestMapping 註解,不配置 method 屬性,那麼 API 文檔會生成 7 種請求方式。
啓動項目,打開瀏覽器訪問 http://localhost:8080/swagger-ui.html。結果如下圖:
十六、參考資料
- Spring Boot 官方文檔
- ML-BLOG (讀者可參考筆者的開源博客源碼學習)
- 本文作者: MoonlightL
- 本文鏈接: https://www.extlight.com/2017/11/24/Spring-Boot-入門之-Web-篇(二)/
- 版權聲明: 本博客所有文章除特別聲明外均爲原創,採用 CC BY-NC-SA 4.0 許可協議。轉載請在文章開頭明顯位置註明原文鏈接和作者等相關信息,明確指出修改(如有),並通過 E-mail 等方式告知,謝謝合作!