一、前言

上一篇《Spring Boot 入门之基础篇(一)》介绍了 Spring Boot 的环境搭建以及项目启动打包等基础内容,本篇继续深入介绍 Spring Boot 与 Web 开发相关的知识。

二、整合模板引擎

由于 jsp 不被 SpringBoot 推荐使用,所以模板引擎主要介绍 Freemarker 和 Thymeleaf。

# 2.1 整合 Freemarker

# 2.1.1 添加 Freemarker 依赖

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-freemarker</artifactId>
  4. </dependency>

# 2.1.2 添加 Freemarker 模板配置

在 application.properties 中添加如下内容:

  1. spring.freemarker.allow-request-override=false
  2. spring.freemarker.cache=true
  3. spring.freemarker.check-template-location=true
  4. spring.freemarker.charset=UTF-8
  5. spring.freemarker.content-type=text/html
  6. spring.freemarker.expose-request-attributes=false
  7. spring.freemarker.expose-session-attributes=false
  8. spring.freemarker.expose-spring-macro-helpers=false
  9. spring.freemarker.prefix=
  10. spring.freemarker.suffix=.ftl

上述配置都是默认值。

# 2.1.3 Freemarker 案例演示

在 controller 包中创建 FreemarkerController:

  1. @Controller
  2. @RequestMapping("freemarker")
  3. public class FreemarkerController {
  4. @RequestMapping("hello")
  5. public String hello(Map<String,Object> map) {
  6. map.put("msg", "Hello Freemarker");
  7. return "hello";
  8. }
  9. }

在 templates 目录中创建名为 hello.ftl 文件,内容如下:

  1. <!DOCTYPE html>
  2. <html lang="zh">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>Document</title>
  6. <link href="/css/index.css" rel="stylesheet"/>
  7. </head>
  8. <body>
  9. <div class="container">
  10. <h2>${msg}</h2>
  11. </div>
  12. </body>
  13. </html>

结果如下:

# 2.2 整合 Thymeleaf

# 2.2.1 添加 Thymeleaf 依赖

在 pom.xml 文件中添加:

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-thymeleaf</artifactId>
  4. </dependency>

# 2.2.2 添加 Thymeleaf 模板配置

在 application.properties 中添加如下内容:

  1. spring.thymeleaf.cache=true
  2. spring.thymeleaf.prefix=classpath:/templates/
  3. spring.thymeleaf.suffix=.html
  4. spring.thymeleaf.mode=HTML5
  5. spring.thymeleaf.encoding=UTF-8
  6. spring.thymeleaf.content-type=text/html

上述配置都是默认值。

# 2.2.3 Thymeleaf 案例演示

在 controller 包中创建 ThymeleafController:

  1. @Controller
  2. @RequestMapping("thymeleaf")
  3. public class ThymeleafController {
  4. @RequestMapping("hello")
  5. public String hello(Map<String,Object> map) {
  6. map.put("msg", "Hello Thymeleaf");
  7. return "hello";
  8. }
  9. }

在 template 目录下创建名为 hello.html 的文件,内容如下:

  1. <!DOCTYPE html>
  2. <html lang="zh">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>Document</title>
  6. <link href="/css/index.css" rel="stylesheet"/>
  7. </head>
  8. <body>
  9. <div class="container">
  10. <h2 th:text="${msg}"></h2>
  11. </div>
  12. </body>
  13. </html>

结果如下:

三、整合 Fastjson

# 3.1 添加依赖

  1. <dependency>
  2. <groupId>com.alibaba</groupId>
  3. <artifactId>fastjson</artifactId>
  4. <version>1.2.35</version>
  5. </dependency>

# 3.2 整合 Fastjson

创建一个配置管理类 WebConfig ,如下:

  1. @Configuration
  2. public class WebConfig {
  3. @Bean
  4. public HttpMessageConverters fastJsonHttpMessageConverters() {
  5. FastJsonHttpMessageConverter fastJsonHttpMessageConverter = new FastJsonHttpMessageConverter();
  6. FastJsonConfig fastJsonConfig = new FastJsonConfig();
  7. fastJsonConfig.setSerializerFeatures(SerializerFeature.PrettyFormat);
  8. fastJsonHttpMessageConverter.setFastJsonConfig(fastJsonConfig);
  9. HttpMessageConverter<?> converter = fastJsonHttpMessageConverter;
  10. return new HttpMessageConverters(converter);
  11. }
  12. }

# 3.3 演示案例:

创建一个实体类 User:

  1. public class User {
  2. private Integer id;
  3. private String username;
  4. private String password;
  5. private Date birthday;
  6. }

getter 和 setter 此处省略。

创建控制器类 FastjsonController :

  1. @Controller
  2. @RequestMapping("fastjson")
  3. public class FastJsonController {
  4. @RequestMapping("/test")
  5. @ResponseBody
  6. public User test() {
  7. User user = new User();
  8. user.setId(1);
  9. user.setUsername("jack");
  10. user.setPassword("jack123");
  11. user.setBirthday(new Date());
  12. return user;
  13. }
  14. }

打开浏览器,访问 http://localhost:8080/fastjson/test,结果如下图:

此时,还不能看出 Fastjson 是否正常工作,我们在 User 类中使用 Fastjson 的注解,如下内容:

  1. @JSONField(format="yyyy-MM-dd")
  2. private Date birthday;

再次访问 http://localhost:8080/fastjson/test,结果如下图:

日期格式与我们修改的内容格式一致,说明 Fastjson 整合成功。

四、自定义 Servlet

# 4.1 编写 Servlet

  1. public class ServletTest extends HttpServlet {
  2. @Override
  3. protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
  4. doPost(req, resp);
  5. }
  6. @Override
  7. protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
  8. resp.setContentType("text/html;charset=utf-8");
  9. resp.getWriter().write("自定义 Servlet");
  10. }
  11. }

# 4.2 注册 Servlet

将 Servelt 注册成 Bean。在上文创建的 WebConfig 类中添加如下代码:

  1. @Bean
  2. public ServletRegistrationBean servletRegistrationBean() {
  3. return new ServletRegistrationBean(new ServletTest(),"/servletTest");
  4. }

结果如下:

五、自定义过滤器/第三方过滤器

# 5.1 编写过滤器

  1. public class TimeFilter implements Filter {
  2. @Override
  3. public void init(FilterConfig filterConfig) throws ServletException {
  4. System.out.println("=======初始化过滤器=========");
  5. }
  6. @Override
  7. public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
  8. throws IOException, ServletException {
  9. long start = System.currentTimeMillis();
  10. filterChain.doFilter(request, response);
  11. System.out.println("filter 耗时:" + (System.currentTimeMillis() - start));
  12. }
  13. @Override
  14. public void destroy() {
  15. System.out.println("=======销毁过滤器=========");
  16. }
  17. }

# 5.2 注册过滤器

要是该过滤器生效,有两种方式:

  1. 使用 @Component 注解

  2. 添加到过滤器链中,此方式适用于使用第三方的过滤器。将过滤器写到 WebConfig 类中,如下:

  1. @Bean
  2. public FilterRegistrationBean timeFilter() {
  3. FilterRegistrationBean registrationBean = new FilterRegistrationBean();
  4. TimeFilter timeFilter = new TimeFilter();
  5. registrationBean.setFilter(timeFilter);
  6. List<String> urls = new ArrayList<>();
  7. urls.add("/*");
  8. registrationBean.setUrlPatterns(urls);
  9. return registrationBean;
  10. }

结果如下:

六、自定义监听器

# 6.1 编写监听器

  1. public class ListenerTest implements ServletContextListener {
  2. @Override
  3. public void contextInitialized(ServletContextEvent sce) {
  4. System.out.println("监听器初始化...");
  5. }
  6. @Override
  7. public void contextDestroyed(ServletContextEvent sce) {
  8. }
  9. }

# 6.2 注册监听器

注册监听器为 Bean,在 WebConfig 配置类中添加如下代码:

  1. @Bean
  2. public ServletListenerRegistrationBean<ListenerTest> servletListenerRegistrationBean() {
  3. return new ServletListenerRegistrationBean<ListenerTest>(new ListenerTest());
  4. }

当启动容器时,结果如下:

针对自定义 Servlet、Filter 和 Listener 的配置,还有另一种方式:

  1. @SpringBootApplication
  2. public class SpringbootWebApplication implements ServletContextInitializer {
  3. @Override
  4. public void onStartup(ServletContext servletContext) throws ServletException {
  5. // 配置 Servlet
  6. servletContext.addServlet("servletTest",new ServletTest())
  7. .addMapping("/servletTest");
  8. // 配置过滤器
  9. servletContext.addFilter("timeFilter",new TimeFilter())
  10. .addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST),true,"/*");
  11. // 配置监听器
  12. servletContext.addListener(new ListenerTest());
  13. }
  14. public static void main(String[] args) {
  15. SpringApplication.run(SpringbootWebApplication.class, args);
  16. }
  17. }

七、自定义拦截器

# 7.1 编写拦截器

使用 @Component 让 Spring 管理其生命周期:

  1. @Component
  2. public class TimeInterceptor implements HandlerInterceptor {
  3. @Override
  4. public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
  5. System.out.println("========preHandle=========");
  6. System.out.println(((HandlerMethod)handler).getBean().getClass().getName());
  7. System.out.println(((HandlerMethod)handler).getMethod().getName());
  8. request.setAttribute("startTime", System.currentTimeMillis());
  9. return true;
  10. }
  11. @Override
  12. public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)
  13. throws Exception {
  14. System.out.println("========postHandle=========");
  15. Long start = (Long) request.getAttribute("startTime");
  16. System.out.println("耗时:"+(System.currentTimeMillis() - start));
  17. }
  18. @Override
  19. public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception exception)
  20. throws Exception {
  21. System.out.println("========afterCompletion=========");
  22. Long start = (Long) request.getAttribute("startTime");
  23. System.out.println("耗时:"+(System.currentTimeMillis() - start));
  24. System.out.println(exception);
  25. }
  26. }

# 7.2 注册拦截器

编写拦截器后,我们还需要将其注册到拦截器链中,如下配置:

  1. @Configuration
  2. public class WebConfig extends WebMvcConfigurerAdapter{
  3. @Autowired
  4. private TimeInterceptor timeInterceptor;
  5. @Override
  6. public void addInterceptors(InterceptorRegistry registry) {
  7. registry.addInterceptor(timeInterceptor);
  8. }
  9. }

请求一个 controller ,结果如下:

八、配置 AOP 切面

# 8.1 添加依赖

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-aop</artifactId>
  4. </dependency>

# 8.2 编写切面类

使用 @Component,@Aspect 标记到切面类上:

  1. @Aspect
  2. @Component
  3. public class TimeAspect {
  4. @Around("execution(* com.light.springboot.controller.FastJsonController..*(..))")
  5. public Object method(ProceedingJoinPoint pjp) throws Throwable {
  6. System.out.println("=====Aspect处理=======");
  7. Object[] args = pjp.getArgs();
  8. for (Object arg : args) {
  9. System.out.println("参数为:" + arg);
  10. }
  11. long start = System.currentTimeMillis();
  12. Object object = pjp.proceed();
  13. System.out.println("Aspect 耗时:" + (System.currentTimeMillis() - start));
  14. return object;
  15. }
  16. }

请求 FastJsonController 控制器的方法,结果如下:

九、错误处理

# 9.1 友好页面

先演示非友好页面,修改 FastJsonController 类中的 test 方法:

  1. @RestController
  2. @RequestMapping("fastjson")
  3. public class FastJsonController {
  4. @RequestMapping("/test")
  5. public User test() {
  6. User user = new User();
  7. user.setId(1);
  8. user.setUsername("jack");
  9. user.setPassword("jack123");
  10. user.setBirthday(new Date());
  11. // 模拟异常
  12. int i = 1/0;
  13. return user;
  14. }
  15. }

浏览器请求:http://localhost:8080/fastjson/test,结果如下:

当系统报错时,返回到页面的内容通常是一些杂乱的代码段,这种显示对用户来说不友好,因此我们需要自定义一个友好的提示系统异常的页面。

在 src/main/resources 下创建 /public/error,在该目录下再创建一个名为 5xx.html 文件,该页面的内容就是当系统报错时返回给用户浏览的内容:

  1. <!DOCTYPE html>
  2. <html lang="zh">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>系统错误</title>
  6. <link href="/css/index.css" rel="stylesheet"/>
  7. </head>
  8. <body>
  9. <div class="container">
  10. <h2>系统内部错误</h2>
  11. </div>
  12. </body>
  13. </html>

路径时固定的,Spring Boot 会在系统报错时将返回视图指向该目录下的文件。

如下图:

上边处理的 5xx 状态码的问题,接下来解决 404 状态码的问题。

当出现 404 的情况时,用户浏览的页面也不够友好,因此我们也需要自定义一个友好的页面给用户展示。

在 /public/error 目录下再创建一个名为 404.html 的文件:

  1. <!DOCTYPE html>
  2. <html lang="zh">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>访问异常</title>
  6. <link href="/css/index.css" rel="stylesheet"/>
  7. </head>
  8. <body>
  9. <div class="container">
  10. <h2>找不到页面</h2>
  11. </div>
  12. </body>
  13. </html>

我们请求一个不存在的资源,如:http://localhost:8080/fastjson/test2,结果如下图:

# 9.2 全局异常捕获

如果项目前后端是通过 JSON 进行数据通信,则当出现异常时可以常用如下方式处理异常信息。

编写一个类充当全局异常的处理类,需要使用 @ControllerAdvice 和 @ExceptionHandler 注解:

  1. @ControllerAdvice
  2. public class GlobalDefaultExceptionHandler {
  3. /**
  4. * 处理 Exception 类型的异常
  5. * @param e
  6. * @return
  7. */
  8. @ExceptionHandler(Exception.class)
  9. @ResponseBody
  10. public Map<String,Object> defaultExceptionHandler(Exception e) {
  11. Map<String,Object> map = new HashMap<String,Object>();
  12. map.put("code", 500);
  13. map.put("msg", e.getMessage());
  14. return map;
  15. }
  16. }

其中,方法名为任意名,入参一般使用 Exception 异常类,方法返回值可自定义。

启动项目,访问 http://localhost:8080/fastjson/test,结果如下图:

我们还可以自定义异常,在全局异常的处理类中捕获和判断,从而对不同的异常做出不同的处理。

十、文件上传和下载

# 10.1 添加依赖

  1. <!-- 工具 -->
  2. <dependency>
  3. <groupId>commons-io</groupId>
  4. <artifactId>commons-io</artifactId>
  5. <version>2.4</version>
  6. </dependency>

# 10.2 实现

编写一个实体类,用于封装返回信息:

  1. public class FileInfo {
  2. private String path;
  3. public FileInfo(String path) {
  4. this.path = path;
  5. }
  6. public String getPath() {
  7. return path;
  8. }
  9. public void setPath(String path) {
  10. this.path = path;
  11. }
  12. }

编写 Controller,用于处理文件上传下载:

  1. @RestController
  2. @RequestMapping("/file")
  3. public class FileController {
  4. private String path = "d:\\";
  5. @PostMapping
  6. public FileInfo upload(MultipartFile file) throws Exception {
  7. System.out.println(file.getName());
  8. System.out.println(file.getOriginalFilename());
  9. System.out.println(file.getSize());
  10. File localFile = new File(path, file.getOriginalFilename());
  11. file.transferTo(localFile);
  12. return new FileInfo(localFile.getAbsolutePath());
  13. }
  14. @GetMapping("/{id}")
  15. public void download(@PathVariable String id, HttpServletRequest request, HttpServletResponse response) {
  16. try (InputStream inputStream = new FileInputStream(new File(path, id + ".jpg"));
  17. OutputStream outputStream = response.getOutputStream();) {
  18. response.setContentType("application/x-download");
  19. response.addHeader("Content-Disposition", "attachment;filename=" + id + ".jpg");
  20. IOUtils.copy(inputStream, outputStream);
  21. } catch (Exception e) {
  22. e.printStackTrace();
  23. }
  24. }
  25. }

基本上都是在学习 javaweb 时用到的 API。

文件上传测试结果如下图:

十一、CORS 支持

前端页面:

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>跨域测试</title>
  6. </head>
  7. <body>
  8. <button id="test">测试</button>
  9. <script type="text/javascript" src="jquery-1.12.3.min.js"></script>
  10. <script type="text/javascript">
  11. $(function() {
  12. $("#test").on("click", function() {
  13. $.ajax({
  14. "url": "http://localhost:8080/fastjson/test",
  15. "type": "get",
  16. "dataType": "json",
  17. "success": function(data) {
  18. console.log(data);
  19. }
  20. })
  21. });
  22. });
  23. </script>
  24. </body>
  25. </html>

通过 http 容器启动前端页面代码,笔者使用 Sublime Text 的插件启动的,测试结果如下:

从图中可知,前端服务器启动端口为 8088 与后端服务器 8080 不同源,因此出现跨域的问题。

现在开始解决跨域问题,可以两种维度控制客户端请求。

粗粒度控制

方式一

  1. @Configuration
  2. public class WebConfig {
  3. @Bean
  4. public WebMvcConfigurer corsConfigurer() {
  5. return new WebMvcConfigurerAdapter() {
  6. @Override
  7. public void addCorsMappings(CorsRegistry registry) {
  8. registry.addMapping("/fastjson/**")
  9. .allowedOrigins("http://localhost:8088");// 允许 8088 端口访问
  10. }
  11. };
  12. }
  13. }

方式二

  1. @Configuration
  2. public class WebConfig extends WebMvcConfigurerAdapter{
  3. @Override
  4. public void addCorsMappings(CorsRegistry registry) {
  5. registry.addMapping("/fastjson/**")
  6. .allowedOrigins("http://localhost:8088");// 允许 8088 端口访问
  7. }
  8. }

配置后,重新发送请求,结果如下:

细粒度控制

在 FastJsonController 类中的方法上添加 @CrossOrigin(origins="xx") 注解:

  1. @RequestMapping("/test")
  2. @CrossOrigin(origins="http://localhost:8088")
  3. public User test() {
  4. User user = new User();
  5. user.setId(1);
  6. user.setUsername("jack");
  7. user.setPassword("jack123");
  8. user.setBirthday(new Date());
  9. return user;
  10. }

在使用该注解时,需要注意 @RequestMapping 使用的请求方式类型,即 GET 或 POST。

十二、整合 WebSocket

# 12.1 添加依赖

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-websocket</artifactId>
  4. </dependency>

# 12.2 实现方式

方式一:

该方式只适用于通过 jar 包直接运行项目的情况。

WebSocket 配置类:

  1. @Configuration
  2. public class WebSocketConfig {
  3. @Bean
  4. public ServerEndpointExporter serverEndpointExporter() {
  5. return new ServerEndpointExporter();
  6. }
  7. }

WebSocket 处理类:

  1. @ServerEndpoint(value = "/webSocketServer/{userName}")
  2. @Component
  3. public class WebSocketServer {
  4. private static final Set<WebSocketServer> connections = new CopyOnWriteArraySet<>();
  5. private String nickname;
  6. private Session session;
  7. private static String getDatetime(Date date) {
  8. SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
  9. return format.format(date);
  10. }
  11. @OnOpen
  12. public void start(@PathParam("userName") String userName, Session session) {
  13. this.nickname = userName;
  14. this.session = session;
  15. connections.add(this);
  16. String message = String.format("* %s %s", nickname, "加入聊天!");
  17. broadcast(message);
  18. }
  19. @OnClose
  20. public void end() {
  21. connections.remove(this);
  22. String message = String.format("* %s %s", nickname, "退出聊天!");
  23. broadcast(message);
  24. }
  25. @OnMessage
  26. public void pushMsg(String message) {
  27. broadcast("【" + this.nickname + "】" + getDatetime(new Date()) + " : " + message);
  28. }
  29. @OnError
  30. public void onError(Throwable t) throws Throwable {
  31. }
  32. private static void broadcast(String msg) {
  33. // 广播形式发送消息
  34. for (WebSocketServer client : connections) {
  35. try {
  36. synchronized (client) {
  37. client.session.getBasicRemote().sendText(msg);
  38. }
  39. } catch (IOException e) {
  40. connections.remove(client);
  41. try {
  42. client.session.close();
  43. } catch (IOException e1) {
  44. e.printStackTrace();
  45. }
  46. String message = String.format("* %s %s", client.nickname, "断开连接");
  47. broadcast(message);
  48. }
  49. }
  50. }
  51. }

前端页面:

  1. <!DOCTYPE html>
  2. <html>
  3. <head lang="zh">
  4. <meta charset="UTF-8">
  5. <link rel="stylesheet" href="css/bootstrap.min.css">
  6. <link rel="stylesheet" href="css/bootstrap-theme.min.css">
  7. <script src="js/jquery-1.12.3.min.js"></script>
  8. <script src="js/bootstrap.js"></script>
  9. <style type="text/css">
  10. #msg {
  11. height: 400px;
  12. overflow-y: auto;
  13. }
  14. #userName {
  15. width: 200px;
  16. }
  17. #logout {
  18. display: none;
  19. }
  20. </style>
  21. <title>webSocket测试</title>
  22. </head>
  23. <body>
  24. <div class="container">
  25. <div class="page-header" id="tou">webSocket及时聊天Demo程序</div>
  26. <p class="text-right" id="logout">
  27. <button class="btn btn-danger" id="logout-btn">退出</button>
  28. </p>
  29. <div class="well" id="msg"></div>
  30. <div class="col-lg">
  31. <div class="input-group">
  32. <input type="text" class="form-control" placeholder="发送信息..." id="message"> <span class="input-group-btn">
  33. <button class="btn btn-default" type="button" id="send"
  34. disabled="disabled">发送</button>
  35. </span>
  36. </div>
  37. <div class="input-group">
  38. <input id="userName" type="text" class="form-control" name="userName" placeholder="输入您的用户名" />
  39. <button class="btn btn-default" type="button" id="connection-btn">建立连接</button>
  40. </div>
  41. <!-- /input-group -->
  42. </div>
  43. <!-- /.col-lg-6 -->
  44. </div>
  45. <!-- /.row -->
  46. </div>
  47. <script type="text/javascript">
  48. $(function() {
  49. var websocket;
  50. $("#connection-btn").bind("click", function() {
  51. var userName = $("#userName").val();
  52. if (userName == null || userName == "") {
  53. alert("请输入您的用户名");
  54. return;
  55. }
  56. connection(userName);
  57. });
  58. function connection(userName) {
  59. var host = window.location.host;
  60. if ('WebSocket' in window) {
  61. websocket = new WebSocket("ws://" + host +
  62. "/webSocketServer/" + userName);
  63. } else if ('MozWebSocket' in window) {
  64. websocket = new MozWebSocket("ws://" + host +
  65. "/webSocketServer/" + userName);
  66. }
  67. websocket.onopen = function(evnt) {
  68. $("#tou").html("链接服务器成功!")
  69. $("#send").prop("disabled", "");
  70. $("#connection-btn").prop("disabled", "disabled");
  71. $("#logout").show();
  72. };
  73. websocket.onmessage = function(evnt) {
  74. $("#msg").html($("#msg").html() + "<br/>" + evnt.data);
  75. };
  76. websocket.onerror = function(evnt) {
  77. $("#tou").html("报错!")
  78. };
  79. websocket.onclose = function(evnt) {
  80. $("#tou").html("与服务器断开了链接!");
  81. $("#send").prop("disabled", "disabled");
  82. $("#connection-btn").prop("disabled", "");
  83. $("#logout").hide();
  84. }
  85. }
  86. function send() {
  87. if (websocket != null) {
  88. var $message = $("#message");
  89. var data = $message.val();
  90. if (data == null || data == "") {
  91. return;
  92. }
  93. websocket.send(data);
  94. $message.val("");
  95. } else {
  96. alert('未与服务器链接.');
  97. }
  98. }
  99. $('#send').bind('click', function() {
  100. send();
  101. });
  102. $(document).on("keypress", function(event) {
  103. if (event.keyCode == "13") {
  104. send();
  105. }
  106. });
  107. $("#logout-btn").on("click", function() {
  108. websocket.close(); //关闭TCP连接
  109. });
  110. });
  111. </script>
  112. </body>
  113. </html>

演示图如下:

如果使用该方式实现 WebSocket 功能并打包成 war 运行会报错:

  1. javax.websocket.DeploymentException: Multiple Endpoints may not be deployed to the same path

方式二:

该方式适用于 jar 包方式运行和 war 方式运行。

WebSocket 配置类:

  1. @Configuration
  2. @EnableWebSocket
  3. public class WebSocketConfig implements WebSocketConfigurer {
  4. @Override
  5. public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
  6. registry.addHandler(webSocketServer(), "/webSocketServer/*");
  7. }
  8. @Bean
  9. public WebSocketHandler webSocketServer() {
  10. return new WebSocketServer();
  11. }
  12. }

WebSocket 处理类:

  1. public class WebSocketServer extends TextWebSocketHandler {
  2. private static final Map<WebSocketSession, String> connections = new ConcurrentHashMap<>();
  3. private static String getDatetime(Date date) {
  4. SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
  5. return format.format(date);
  6. }
  7. /**
  8. * 建立连接
  9. */
  10. @Override
  11. public void afterConnectionEstablished(WebSocketSession session) throws Exception {
  12. String uri = session.getUri().toString();
  13. String userName = uri.substring(uri.lastIndexOf("/") + 1);
  14. String nickname = URLDecoder.decode(userName, "utf-8");
  15. connections.put(session, nickname);
  16. String message = String.format("* %s %s", nickname, "加入聊天!");
  17. broadcast(new TextMessage(message));
  18. }
  19. /**
  20. * 断开连接
  21. */
  22. @Override
  23. public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
  24. String nickname = connections.remove(session);
  25. String message = String.format("* %s %s", nickname, "退出聊天!");
  26. broadcast(new TextMessage(message));
  27. }
  28. /**
  29. * 处理消息
  30. */
  31. @Override
  32. protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
  33. String msg = "【" + connections.get(session) + "】" + getDatetime(new Date()) + " : " + message.getPayload();
  34. broadcast(new TextMessage(msg));
  35. }
  36. private static void broadcast(TextMessage msg) {
  37. // 广播形式发送消息
  38. for (WebSocketSession session : connections.keySet()) {
  39. try {
  40. synchronized (session) {
  41. session.sendMessage(msg);
  42. }
  43. } catch (Exception e) {
  44. connections.remove(session);
  45. try {
  46. session.close();
  47. } catch (Exception e2) {
  48. e2.printStackTrace();
  49. }
  50. String message = String.format("* %s %s", connections.get(session), "断开连接");
  51. broadcast(new TextMessage(message));
  52. }
  53. }
  54. }
  55. }

运行结果与上图一致。

十三、整合 JavaMail

本次测试演示带模板的邮件,使用 Freemark 实现邮件的模板。

# 13.1 添加依赖

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-mail</artifactId>
  4. </dependency>
  5. <dependency>
  6. <groupId>org.springframework.boot</groupId>
  7. <artifactId>spring-boot-starter-freemarker</artifactId>
  8. </dependency>

# 13.2 添加配置

在 application.properties 中添加

  1. # javamail 配置
  2. spring.mail.host=smtp.163.com
  3. spring.mail.username=13738137546@163.com
  4. spring.mail.password=
  5. spring.mail.properties.mail.smtp.auth=true
  6. spring.mail.properties.mail.smtp.starttls.enable=true
  7. spring.mail.properties.mail.smtp.starttls.required=true

# 13.3 编码

  1. @Component
  2. @EnableConfigurationProperties(MailProperties.class)
  3. public class JavaMailComponent {
  4. private static final String template = "mail.ftl";
  5. @Autowired
  6. private FreeMarkerConfigurer freeMarkerConfigurer;
  7. @Autowired
  8. private JavaMailSender javaMailSender;
  9. @Autowired
  10. private MailProperties mailProperties;
  11. public void sendMail(String email) {
  12. Map<String, Object> map = new HashMap<String, Object>();
  13. map.put("email", email);
  14. try {
  15. // 获取内容
  16. String text = this.getTextByTemplate(template, map);
  17. // 发送
  18. this.send(email, text);
  19. } catch (Exception e) {
  20. e.printStackTrace();
  21. }
  22. }
  23. private String getTextByTemplate(String template, Map<String, Object> model) throws Exception {
  24. return FreeMarkerTemplateUtils
  25. .processTemplateIntoString(this.freeMarkerConfigurer.getConfiguration().getTemplate(template), model);
  26. }
  27. private String send(String email, String text) throws MessagingException, UnsupportedEncodingException {
  28. MimeMessage message = this.javaMailSender.createMimeMessage();
  29. MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
  30. InternetAddress from = new InternetAddress();
  31. from.setAddress(this.mailProperties.getUsername());
  32. from.setPersonal("月光中的污点", "UTF-8");
  33. helper.setFrom(from);
  34. helper.setTo(email);
  35. helper.setSubject("SpringBoot 发送的第一封邮件");
  36. helper.setText(text, true);
  37. this.javaMailSender.send(message);
  38. return text;
  39. }
  40. }

在 src/main/resources 下的 template 目录下创建名为 mail.ftl 的文件,其内容如下:

  1. <!DOCTYPE html>
  2. <html lang="zh">
  3. <head>
  4. <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
  5. </head>
  6. <body>
  7. <div style="width: 600px; text-align: left; margin: 0 auto;">
  8. <h1 style="color: #005da7;">月光中的污点</h1>
  9. <div style="border-bottom: 5px solid #005da7; height: 2px; width: 100%;"></div>
  10. <div style="border: 1px solid #005da7; font-size: 16px; line-height: 50px; padding: 20px;">
  11. <div>${email},您好!</div>
  12. <div>
  13. 这是个测试
  14. </div>
  15. <div>
  16. 想了解更多信息,请访问 <a href="https://www.extlight.com">https://www.extlight.com</a>
  17. </div>
  18. </div>
  19. </div>
  20. </body>
  21. </html>

# 13.4 测试

  1. @RunWith(SpringRunner.class)
  2. @SpringBootTest
  3. public class MailTest {
  4. @Autowired
  5. private JavaMailComponent javaMailComponent;
  6. @Test
  7. public void test() {
  8. this.javaMailComponent.sendMail("445847261@qq.com");
  9. }
  10. }

运行结果如下图:

十四、整合定时任务

定时器的实现有 2 种方式:

1) Scheduled:spring 3.0 后自带的定时器

2)Quartz:第三放定时器框架

# 14.1 Scheduled 方式

# 14.1.1 任务类

  1. @Component
  2. public class Schedule {
  3. @Scheduled(fixedRate = 2000)
  4. public void task() {
  5. System.out.println("启动定时任务:" + new Date());
  6. }
  7. }

使用 @Scheduled 定义任务执行时间,代码中表示每隔 2 秒执行一次任务。

# 14.1.2 开启定时计划

只需在 Spring Boot 的启动类上添加 @EnableScheduling 后,启动项目即可。

测试结果如下图:

# 14.2 Quartz 方式

# 14.2.1 任务类

  1. public class MyJob implements Job {
  2. @Override
  3. public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
  4. System.out.println("========quartz 测试==========");
  5. }
  6. }

# 14.2.2 配置类

  1. @Configuration
  2. public class QuartzConfiguration {
  3. /**
  4. * Job 工厂
  5. * @return
  6. */
  7. @Bean
  8. public JobDetailFactoryBean jobDetailFactoryBean() {
  9. JobDetailFactoryBean factory = new JobDetailFactoryBean();
  10. factory.setJobClass(MyJob.class);
  11. return factory;
  12. }
  13. /**
  14. * Trigger 工厂
  15. * @return
  16. */
  17. @Bean
  18. public SimpleTriggerFactoryBean simpleTriggerFactoryBean(JobDetailFactoryBean jobDetailFactory) {
  19. SimpleTriggerFactoryBean factory = new SimpleTriggerFactoryBean();
  20. factory.setJobDetail(jobDetailFactory.getObject());
  21. // 执行间隔时间
  22. factory.setRepeatInterval(5000);
  23. // 重复执行次数
  24. factory.setRepeatCount(3);
  25. return factory;
  26. }
  27. /**
  28. * Trigger 工厂
  29. * @return
  30. */
  31. @Bean
  32. public CronTriggerFactoryBean cronTriggerFactoryBean(JobDetailFactoryBean jobDetailFactory) {
  33. CronTriggerFactoryBean factory = new CronTriggerFactoryBean();
  34. factory.setJobDetail(jobDetailFactory.getObject());
  35. factory.setCronExpression("0/5 * * * * ?");
  36. return factory;
  37. }
  38. /* @Bean
  39. public SchedulerFactoryBean schedulerFactoryBean(SimpleTriggerFactoryBean simpleTriggerFactory){
  40. SchedulerFactoryBean factory = new SchedulerFactoryBean();
  41. factory.setTriggers(simpleTriggerFactory.getObject());
  42. return factory;
  43. }*/
  44. @Bean
  45. public SchedulerFactoryBean schedulerFactoryBean(CronTriggerFactoryBean cronTriggerFactory){
  46. SchedulerFactoryBean factory = new SchedulerFactoryBean();
  47. factory.setTriggers(cronTriggerFactory.getObject());
  48. return factory;
  49. }
  50. }

同样地,需要在 Spring Boot 的启动类上添加 @EnableScheduling 后,启动项目即可。

# 14.2.3 依赖注入问题

实际开发中,任务类需要注入业务组件来执行定时任务,如下:

  1. public class MyJob implements Job {
  2. @Autowired
  3. private UserService userService;
  4. @Override
  5. public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
  6. this.userService.save();
  7. }
  8. }

但是,MyJob 生命周期并没有被 Spring 容器管理,因此无法注入 UserService,当定时器执行任务时会报空指针异常。

解决方案:

自定义任务工厂,重写创建任务实例的方法:

  1. @Component("customAdaptableJobFactory")
  2. public class CustomAdaptableJobFactory extends AdaptableJobFactory {
  3. @Autowired
  4. private AutowireCapableBeanFactory autowireCapableBeanFactory;
  5. @Override
  6. protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception {
  7. Object object = super.createJobInstance(bundle);
  8. // 将任务实例纳入 Spring 容器中
  9. this.autowireCapableBeanFactory.autowireBean(object);
  10. return object;
  11. }
  12. }

修改 Scheduler 实现:

  1. @Bean
  2. public SchedulerFactoryBean schedulerFactoryBean(CronTriggerFactoryBean cronTriggerFactory,CustomAdaptableJobFactory customAdaptableJobFactory){
  3. SchedulerFactoryBean factory = new SchedulerFactoryBean();
  4. factory.setTriggers(cronTriggerFactory.getObject());
  5. factory.setJobFactory(customAdaptableJobFactory);
  6. return factory;
  7. }

十五、整合 Swagger2

# 15.1 添加依赖

  1. <dependency>
  2. <groupId>io.springfox</groupId>
  3. <artifactId>springfox-swagger2</artifactId>
  4. <version>2.7.0</version>
  5. </dependency>
  6. <dependency>
  7. <groupId>io.springfox</groupId>
  8. <artifactId>springfox-swagger-ui</artifactId>
  9. <version>2.7.0</version>
  10. </dependency>

# 15.2 配置

重新创建一个配置类,如下:

  1. @Configuration
  2. @EnableSwagger2
  3. public class Swagger2Configuration {
  4. @Bean
  5. public Docket accessToken() {
  6. return new Docket(DocumentationType.SWAGGER_2)
  7. .groupName("api")// 定义组
  8. .select() // 选择那些路径和 api 会生成 document
  9. .apis(RequestHandlerSelectors.basePackage("com.light.springboot.controller")) // 拦截的包路径
  10. .paths(PathSelectors.regex("/*/.*"))// 拦截的接口路径
  11. .build() // 创建
  12. .apiInfo(apiInfo()); // 配置说明
  13. }
  14. private ApiInfo apiInfo() {
  15. return new ApiInfoBuilder()//
  16. .title("Spring Boot 之 Web 篇")// 标题
  17. .description("spring boot Web 相关内容")// 描述
  18. .termsOfServiceUrl("http://www.extlight.com")//
  19. .contact(new Contact("moonlightL", "http://www.extlight.com", "445847261@qq.com"))// 联系
  20. .version("1.0")// 版本
  21. .build();
  22. }
  23. }

为了能更好的说明接口信息,我们还可以在 Controller 类上使用 Swagger2 相关注解说明信息。

我们以 FastJsonController 为例:

  1. @Api(value = "FastJson测试", tags = { "测试接口" })
  2. @RestController
  3. @RequestMapping("fastjson")
  4. public class FastJsonController {
  5. @ApiOperation("获取用户信息")
  6. @ApiImplicitParam(name = "name", value = "用户名", dataType = "string", paramType = "query")
  7. @GetMapping("/test/{name}")
  8. public User test(@PathVariable("name") String name) {
  9. User user = new User();
  10. user.setId(1);
  11. user.setUsername(name);
  12. user.setPassword("jack123");
  13. user.setBirthday(new Date());
  14. return user;
  15. }
  16. }

注意,上边的方法是用 @GetMapping 注解,如果只是使用 @RequestMapping 注解,不配置 method 属性,那么 API 文档会生成 7 种请求方式。

启动项目,打开浏览器访问 http://localhost:8080/swagger-ui.html。结果如下图:

十六、参考资料