面试题:

面试题1:简述一下Springboot相对SSM做了哪些提升?

 首先,SpringBoot是采用“约定大于配置”(Convention over Configuration)的理念,让你的项目快速运行起来,简化了大量的XML配置。使用SpringBoot很容易创建一个独立运行(运行jar,内嵌Servlet容器)、准生产级别的基于Spring框架的项目,使用SpringBoot你可以不用或者只用很少的Spring配置即可。

以前,你不仅要实现业务需求,还要自己配置SpringMVC、Spring、Mybatis相关配置信息。而Spring Boot做到自动配置,让你腾出精力和时间更多的去关心核心业务。

SpringBoot的优点?
1 . Spring由于其繁琐的配置,各种XML、Annotation配置,让人眼花缭乱,而且如果出错了也很难找出原因,而SpringBoot友好的内置了很多通用配置信息。
2 . SpringBoot帮助开发者快速启动一个Web容器;
3 . SpringBoot提供了更多的组合式注解,简化开发过程;
4 . SpringBoot可以”零配置”整合很多第三方工具;
5 . SpringBoot继承了原有Spring框架的优秀基因;

追问1:说说你在使用SpringBoot时比较有印象的有哪些注解?

1、@SpringBootApplication:
1 . 这个注解是Spring Boot最核心的注解,用在 Spring Boot的主类上,标识这是一个 Spring Boot 应用,用来开启 Spring Boot 的各项能力。
2 . 实际上这个注解是@Configuration,@EnableAutoConfiguration,@ComponentScan三个注解的组合。由于这些注解一般都是一起使用,所以Spring Boot提供了一个统一的注解@SpringBootApplication。

追问2:@RequestMapping注解的属性有哪些?分别都是干什么用的?

image.png

面试题2:SpringBoot 打成的 jar 包和普通的 jar 包有什么区别?

Spring Boot 中默认打包成的 jar 叫做**可执行 jar**,这种jar包可以通过可以通过命令(java -jar xxx.jar)来运行的,但这种jar包不能被其他项目所依赖,因为它和普通 jar 的结构不同,即使被依赖了也不能直接使用其中的类。
image.png
普通的jar包,解压后直接就是包名,包里就是我们的代码,而 Spring Boot 打包成的可执行 jar 解压后,在 \BOOT-INF\classes 目录下才是我们的代码,因此无法被直接引用。如果非要引用,可以在 pom.xml 文件中增加配置,将 Spring Boot 项目打包成两个 jar ,一个可执行,一个可引用。
image.png

追问1:如何让SpringBoot打的jar包可依赖?

在pom文件中增加以下配置



org.springframework.boot
spring-boot-maven-plugin




repackage



exec





maven-compiler-plugin

1.8
1.8




image.png

面试题3:CORS跨域问题是怎么引起的呢?

Springboot跨域问题,是当前主流web开发人员都绕不开的难题。但我们首先要明确以下几点:
1 . 跨域只存在于浏览器端,不存在于安卓/ios/Node.js/python/ java等其它环境
2 . 跨域请求能发出去,服务端能收到请求并正常返回结果,只是结果被浏览器拦截了。
3 . 之所以会跨域,是因为受到了同源策略的限制,同源策略要求源相同才能正常进行通信,即协议、域名、端口号都完全一致。

 浏览器出于安全的考虑,使用 XMLHttpRequest对象发起 HTTP请求时必须遵守同源策略,否则就是跨域的HTTP请求,默认情况下是被禁止的。换句话说,浏览器安全的基石是同源策略。
image.pngimage.pngimage.png

追问1:处理过Springboot的CORS跨域问题么?怎么解决的?

方法一、直接采用SpringBoot的注解@CrossOrigin(也支持SpringMVC):
image.png方法二、处理跨域请求的Configuration:
  增加一个配置类,CrossOriginConfig.java。继承WebMvcConfigurerAdapter或者实现WebMvcConfigurer接口,其他都不用管,项目启动时,会自动读取配置。
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

/
AJAX请求跨域
@author Mr.W
@time 2018-08-13
/
@Configuration
public class CorsConfig extends WebMvcConfigurerAdapter {
static final String ORIGINS[] = new String[] { “GET”, “POST”, “PUT”, “DELETE” };
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping(“/“).allowedOrigins(“*”).allowCredentials(true).allowedMethods(ORIGINS).maxAge(3600);
}
}

方法三、采用过滤器(filter)的方式:
  同方法二加配置类,增加一个CORSFilter 类,并实现Filter接口即可,其他都不用管,接口调用时,会过滤跨域的拦截。
@Component
public class CORSFilter implements Filter {

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletResponse res = (HttpServletResponse) response;
res.addHeader(“Access-Control-Allow-Credentials”, “true”);
res.addHeader(“Access-Control-Allow-Origin”, “*”);
res.addHeader(“Access-Control-Allow-Methods”, “GET, POST, DELETE, PUT”);
res.addHeader(“Access-Control-Allow-Headers”, “Content-Type,X-CAF-Authorization-Token,sessionToken,X-TOKEN”);
if (((HttpServletRequest) request).getMethod().equals(“OPTIONS”)) {
response.getWriter().println(“ok”);
return;
}
chain.doFilter(request, response);
}
@Override
public void destroy() {
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
}

springBoot用法:

1.配置springboot文件上传路径:

1.1:配置路径配置类:

  1. package com.example.blogdemo.configuration;
  2. import org.springframework.beans.factory.annotation.Autowired;
  3. import org.springframework.context.annotation.Configuration;
  4. import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
  5. import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
  6. /**
  7. * 配置文件上传路径
  8. * */
  9. @Configuration
  10. public class WebMvcConfig extends WebMvcConfigurerAdapter {
  11. @Autowired
  12. private FileUploadProperties fileUploadProperties;
  13. @Override
  14. public void addResourceHandlers(ResourceHandlerRegistry registry) {
  15. //获取jar包所在目录
  16. // ApplicationHome h = new ApplicationHome(getClass());
  17. // File jarF = h.getSource();
  18. //在jar包所在目录下生成一个upload文件夹用来存储上传的图片
  19. //jarbao:D:\blog\tylblog\blogdemo\target/upload/
  20. // String imgPuth = jarF.getParentFile().toString()+"/upload/";
  21. registry.addResourceHandler(fileUploadProperties.getStaticAccessPath()).addResourceLocations("file:/"+fileUploadProperties.getUploadFolder());
  22. }
  23. }

1.2:配置yml

  1. file:
  2. staticAccessPath: /uploadBlogImg/**
  3. uploadFolder: D:\\blog\\tylblog\\uploadBlogImg\\

1.3:使用:

  1. package com.example.blogdemo.service.serviceImpl;
  2. import com.example.blogdemo.configuration.FileUploadProperties;
  3. import com.example.blogdemo.service.FileUploadService;
  4. import com.example.blogdemo.vo.WangEditorVO;
  5. import org.springframework.beans.factory.annotation.Autowired;
  6. import org.springframework.boot.system.ApplicationHome;
  7. import org.springframework.stereotype.Service;
  8. import org.springframework.util.ClassUtils;
  9. import org.springframework.web.multipart.MultipartFile;
  10. import javax.servlet.http.HttpServletRequest;
  11. import javax.servlet.http.HttpServletResponse;
  12. import java.io.File;
  13. import java.io.IOException;
  14. import java.util.ArrayList;
  15. import java.util.List;
  16. import java.util.UUID;
  17. @Service
  18. public class FileUploadServiceImpl implements FileUploadService {
  19. @Autowired
  20. private FileUploadProperties fileUploadProperties;
  21. // private String imgPuth= null ;
  22. private static final String SERVER_PREFIX = "http://localhost:8081/uploadBlogImg/";
  23. @Override
  24. public WangEditorVO handleFileUpload(MultipartFile[] file) throws IOException {
  25. //当file为空时
  26. if (file == null || file.length == 0) {
  27. return WangEditorVO.error(500, "无图片信息");
  28. }
  29. //获取jar包所在目录
  30. // ApplicationHome h = new ApplicationHome(getClass());
  31. // File jarF = h.getSource();
  32. // //在jar包所在目录下生成一个upload文件夹用来存储上传的图片
  33. // imgPuth = jarF.getParentFile().toString()+"/upload/";
  34. //存入数据库的文件地址集合
  35. List<String> pathList = new ArrayList<>();
  36. for (MultipartFile multipartFile : file) {
  37. //获取文件名
  38. String name = UUID.randomUUID().toString();
  39. //拼接完整的 存放图片地址,即:D:\\IO\\shop\\shopImage\\文件名.后缀名
  40. File filePath = new File(fileUploadProperties.getUploadFolder() + name + getSuffix(multipartFile));
  41. if(!filePath.exists()){
  42. filePath.mkdirs();
  43. }
  44. //将图片存放到path路径下
  45. multipartFile.transferTo(filePath);
  46. //拼接完整的 访问图片地址,即:http://localhost:8888/文件名.后缀名
  47. pathList.add(SERVER_PREFIX + name + getSuffix(multipartFile));
  48. }
  49. return WangEditorVO.success(pathList);
  50. }
  51. /**
  52. * 获取文件的后缀名
  53. *
  54. * @param multipartFile 上传的文件
  55. * @return 文件的后缀名
  56. */
  57. private String getSuffix(MultipartFile multipartFile) {
  58. //获取完整的文件名
  59. String fileName = multipartFile.getOriginalFilename();
  60. //截取后缀
  61. String fileSuffix = fileName.substring(fileName.lastIndexOf("."));
  62. return fileSuffix;
  63. }
  64. }

2.SpringDat redis的使用

2.1 .导入依赖

  1. <!-- spring data redis依赖 -->
  2. <dependency>
  3. <groupId>org.springframework.boot</groupId>
  4. <artifactId>spring-boot-starter-data-redis</artifactId>
  5. </dependency>
  6. <!-- commons-pool2对象池依赖 -->
  7. <dependency>
  8. <groupId>org.apache.commons</groupId>
  9. <artifactId>commons-pool2</artifactId>
  10. </dependency>

2.2 配置application.yaml

  1. redis:
  2. #超时时间
  3. timeout: 10000ms
  4. #服务器地址
  5. host: 192.168.31.115
  6. #服务器端口
  7. port: 6379
  8. #数据库
  9. database: 0
  10. #密码
  11. password: root
  12. lettuce:
  13. pool:
  14. #最大连接数 默认8
  15. max-active: 1024
  16. #最大连接阻塞等待时间, 默认-1
  17. max-wait: 10000ms
  18. #最大空闲连接
  19. max-idle: 200
  20. #最小空闲连接
  21. min-idle: 5

2.3使用RedisTemplate.opsForvalue();获取letts对象

3.搭建hello world:

3.1:第一步放入依赖:

  1. <!--超级父pom 是springboot父依赖 声明版本号-->
  2. <parent>
  3. <groupId>org.springframework.boot</groupId>
  4. <artifactId>spring-boot-starter-parent</artifactId>
  5. <version>2.1.8.RELEASE</version>
  6. </parent>
  7. <dependencies>
  8. <!--web项目需要使用的-->
  9. <dependency>
  10. <groupId>org.springframework.boot</groupId>
  11. <artifactId>spring-boot-starter-web</artifactId>
  12. </dependency>
  13. </dependencies>

3.2:打开程序入口:

  1. /**
  2. * @EnableConfigurationProperties({FoodConfig.class})
  3. * 告诉主程序入口类 要自动引入配置文件
  4. * 配置文件对应的类作为它的参数
  5. */
  6. @SpringBootApplication
  7. @EnableConfigurationProperties({FoodConfig.class})
  8. public class DemoApplication {
  9. public static void main(String[] args) {
  10. //使用SpringApplication类的静态方法 启动springboot程序
  11. //方法的参数 程序的入口类 main函数的参数
  12. SpringApplication.run(DemoApplication.class,args);
  13. }
  14. }

3.3编写业务逻辑:

  1. /**
  2. * @Controller
  3. * 控制类 业务逻辑 请求分发 组装响应
  4. */
  5. @Controller
  6. public class HelloController {
  7. /**
  8. * @RequestMapping
  9. * 指定方法和请求之间的映射关系
  10. * @return
  11. */
  12. @RequestMapping("/hello")
  13. @ResponseBody
  14. public String hello(){
  15. return "Hello Duing";
  16. }
  17. }

4.lombok的使用:

4.1 idea安装插件:

File -> setting -> plugins -> 查找lombok后安装重启
File -> Setting -> Compiler -> Annotation Processors -> Enable annotation processing勾选

4.2 pom引入依赖:

  1. <dependency>
  2. <groupId>org.projectlombok</groupId>
  3. <artifactId>lombok</artifactId>
  4. <version>1.18.10</version>
  5. </dependency>

4.3lombok插件常用注解:

4.3.1:@Getter / @Setter:可以作用在类上和属性上,放在类上,会对所有的非静态(non-static)属性生成
Getter/Setter方法,放在属性上,会对该属性生成Getter/Setter方法。并可以指Getter/Sette的
访问级别。
4.3.2:@EqualsAndHashCode :默认情况下,会使用所有非瞬态(non-transient)和非静态(non-static)字段来生成equals和hascode方法,也可以指定具体使用哪些属性。 @ToString 生成toString方法,默认情况下,会输出类名、所有属性,属性会按照顺序输出,以逗号分割。
4.3.3:@NoArgsConstructor, @RequiredArgsConstructor and @AllArgsConstructor:无参构造器、部分参:数构造器、全参构造器
4.3.4:@Data:包含@ToString, @EqualsAndHashCode, 所有属性的@Getter, 所有non-final属性的@Setter和@RequiredArgsConstructor的组合,通常情况下,基本上使用这个注解就足够了。
4.3.5:@Budilder:可以进行Builder方式初始化。
4.3.6:@Slf4j:等同于:private final Logger logger = LoggerFactory.getLogger(XXX.class);

5.springboot3种启动方式:

5.1:Application-run

5.2:mvn spring-boot:run

5.3:mvn packag —java -jar

6.springboot多环境配置:

6.1:Multiple Application:

目前使用比较多的是配置多个 application-{profile}.yml 或者多个 application-{profile}.properties文件的写法,一张图就能解释清楚了,如下图所示。
image.png
application.yml 文件用于配置各个环境通用的配置,在这里我指定了程序使用的 profile,即 spring.profile.active= ${env:dev}属性,它的值决定了具体使用的配置。

例如 spring.profile.active=dev 时,使用 application-dev.yml + application.yml 的配置。

运行jar包的时候通过设置spring.profile.active=xxx 来指定运行哪个环境,例如使用 dev 环境:

  1. java -jar xxx.jar --spring.profiles.active=dev

7.springboot YAML使用:

7.1:语法规则:

1.大小写敏感
2.使用缩进表示层级关系
3.缩进时不允许适应tab键,只允许适应空格
4.相同层级左侧要对齐
5.#表示注释,从这个字符一直到尾,都会被解析器忽略

7.2:YAML 支持的数据结构:

对象:键值对的集合,又称为映射(mapping)/ 哈希(hashes) / 字典(dictionary)
数组:一组按次序排列的值,又称为序列(sequence) / 列表(list)
纯量(scalars):单个的、不可再分的值

7.2.1:对象的使用:

image.png

7.2.2:数组的使用:

image.png

7.2.3:YAML 纯量:

image.png

8.Thymeleaf的使用:

8.1 pom.xml中加入thymeleaf依赖

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

8.2 在main/resources目录下,创建templates文件夹,创建html文件

文件中引入<html xmlns:th=”http://www.thymeleaf.org”>,获取对标签的提示

name

8.3 编写controller

  1. HelloController.java
  2. @RequestMapping("/hello")
  3. public String hello(Model m){
  4. // m.addAttribute("name", "thymeleaf");
  5. m.addAttribute("name", "<span style='color:red'>thymeleaf</span>");
  6. return "hello";
  7. }

8.4 Hello.html

  1. <!--转义和非转义的html th:text会对<和>进行转义,而th:utext不会转义。-->
  2. <p th:utext="${name}" >name</p>
  3. <p th:text="${name}" >name</p>

Thymeleaf变量表达式:

image.png

Thymeleaf选择变量表达式:

image.png

Thymeleaf链接URL表达式:

image.png

Thymeleaf消息表达式:

image.png

Thymeleaf片段表达式:

image.png

Thymeleaf语法:

image.png

9.springBoot 整合Mybatis:

9.1 引入依赖:

  1. <dependency>
  2. <groupId>org.mybatis.spring.boot</groupId>
  3. <artifactId>mybatis-spring-boot-starter</artifactId>
  4. <version>2.1.0</version>
  5. </dependency>
  6. <dependency>
  7. <groupId>mysql</groupId>
  8. <artifactId>mysql-connector-java</artifactId>
  9. <scope>runtime</scope>
  10. </dependency>

9.2 在application.properties中配置数据源:

  1. spring.datasource.url=jdbc:mysql://127.0.0.1:3306/mytestdb
  2. spring.datasource.username=root
  3. spring.datasource.password=123456
  4. #默认根据url识别
  5. #spring.datasource.driver-class-name=com.mysql.jdbc.Driver
  6. #使用mybatis-config.xml时的配置项
  7. #mybatis.config-location=classpath:mybatis-config.xml
  8. #代替mybatis-config.xml的配置项
  9. #mybatis.mapper-locations=classpath*:/mappers/*.xml
  10. #mybatis.type-aliases-package=sample.mybatis.domain

9.2.1 properties.yml中的配置:

  1. # 服务端口号
  2. server:
  3. port: 8080
  4. # 数据库地址
  5. datasource:
  6. url: localhost:3306/blog_test
  7. spring:
  8. datasource: # 数据库配置
  9. driver-class-name: com.mysql.jdbc.Driver
  10. url: jdbc:mysql://${datasource.url}?useSSL=false&useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&autoReconnect=true&failOverReadOnly=false&maxReconnects=10
  11. username: root
  12. password: 123456
  13. hikari:
  14. maximum-pool-size: 10 # 最大连接池数
  15. max-lifetime: 1770000
  16. mybatis:
  17. # 指定别名设置的包为所有entity
  18. type-aliases-package: com.itcodai.course10.entity
  19. configuration:
  20. map-underscore-to-camel-case: true # 驼峰命名规范
  21. mapper-locations: # mapper映射文件位置
  22. - classpath:mapper/*.xml

9.3 Mybatis基于xml配置文件的查询方式:

使用原始的 xml 方式,需要新建 UserMapper.xml 文件,在上面的 application.yml 配置文件中,我们已经定义了 xml 文件的路径:classpath:mapper/*.xml,所以我们在 resources 目录下新建一个 mapper 文件夹,然后创建一个 UserMapper.xml 文件。

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
  3. <mapper namespace="com.itcodai.course10.dao.UserMapper">
  4. <resultMap id="BaseResultMap" type="com.itcodai.course10.entity.User">
  5. <id column="id" jdbcType="BIGINT" property="id" />
  6. <result column="user_name" jdbcType="VARCHAR" property="username" />
  7. <result column="password" jdbcType="VARCHAR" property="password" />
  8. </resultMap>
  9. <select id="getUserByName" resultType="User" parameterType="String">
  10. select * from user where user_name = #{username}
  11. </select>
  12. </mapper>

9.3 Mybatis整合之注解方式:

3 在启动类上增加对mapper的包扫描注解 @MapperScan
如果不使用@MapperScan注解,还可以在每个 mapper 接口类上加上 @Mapper 这个注解(不推荐)
4 开发Mapper类,编写sql, 提供给service调用

  1. @Select("select name,role from guest")
  2. @Delete("delete from guest where name=#{name}")
  3. @Update("update guest set role=#{role} where name=#{name}")
  4. @Options(useGeneratedKeys = true,keyProperty = "id")
  5. @Insert("insert into guest(name,role) values(#{name},#{role})")

10. springBoot Mybatis-plus:

10.1 导入依赖:

  1. <!-- mybatis plus 代码生成器 -->
  2. <dependency>
  3. <groupId>com.baomidou</groupId>
  4. <artifactId>mybatis-plus-boot-starter</artifactId>
  5. <version>3.2.0</version>
  6. </dependency>
  7. <dependency>
  8. <groupId>com.baomidou</groupId>
  9. <artifactId>mybatis-plus-generator</artifactId>
  10. <version>3.2.0</version>
  11. </dependency>
  12. <dependency>

10.2 配置application.yml:

  1. server:
  2. port: 8081
  3. servlet:
  4. context-path: /
  5. spring:
  6. datasource:
  7. driver-class-name: com.mysql.cj.jdbc.Driver
  8. url: jdbc:mysql://127.0.0.1:3306/demo?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true
  9. username: root
  10. password: lyja
  11. jackson:
  12. date-format: yyyy-MM-dd HH:mm:ss
  13. time-zone: GMT+8
  14. serialization:
  15. write-dates-as-timestamps: false
  16. mybatis-plus:
  17. configuration:
  18. map-underscore-to-camel-case: true
  19. auto-mapping-behavior: full
  20. log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  21. mapper-locations: classpath*:mapper/**/*Mapper.xml
  22. global-config:
  23. # 逻辑删除配置
  24. db-config:
  25. # 删除前
  26. logic-not-delete-value: 1
  27. # 删除后
  28. logic-delete-value: 0

10.3 mybatisplus分页插件MybatisPlusConfig:

  1. package com.example.conf;
  2. import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor;
  3. import org.springframework.context.annotation.Bean;
  4. import org.springframework.context.annotation.Configuration;
  5. /**
  6. * 配置分页插件
  7. *
  8. */
  9. @Configuration
  10. public class MybatisPlusConfig {
  11. /**
  12. * 分页插件
  13. */
  14. @Bean
  15. public PaginationInterceptor paginationInterceptor() {
  16. return new PaginationInterceptor();
  17. }
  18. }

10.4 创建数据库 创建对应实体类

10.5编写操作实体类的Mapper类(直接继承BaseMapper):

ps:BaseMapper :UserTest是自己定义对应实体类的类名
ps:运用mybatis-plus 记得在启动类上面添加@MapperScan注解
image.png

BaseMapper里面的对应的方法:
image.png

10.6 mybatis-plus常用注解:

  1. @TableName 作用于类上
  2. @TableName 用于定义表名
  3. 注:
  4. 常用属性:
  5. value 用于定义表名
  6. @TableId 作用于属性上
  7. @TableId 用于定义表的主键
  8. 注:
  9. 常用属性:
  10. value 用于定义主键字段名
  11. type 用于定义主键类型(主键策略 IdType
  12. 主键策略:
  13. IdType.AUTO 主键自增,系统分配,不需要手动输入
  14. IdType.NONE 未设置主键
  15. IdType.INPUT 需要自己输入 主键值。
  16. IdType.ASSIGN_ID 系统分配 ID,用于数值型数据(Long,对应 mysql BIGINT 类型)。
  17. IdType.ASSIGN_UUID 系统分配 UUID,用于字符串型数据(String,对应 mysql varchar(32) 类型)。
  18. @TableField 作用与属性上
  19. @TableField 用于定义表的非主键字段。
  20. 注:
  21. 常用属性:
  22. value 用于定义非主键字段名
  23. exist 用于指明是否为数据表的字段, true 表示是,false 为不是。
  24. fill 用于指定字段填充策略(FieldFill)。
  25. 字段填充策略:(一般用于填充 创建时间、修改时间等字段)
  26. FieldFill.DEFAULT 默认不填充
  27. FieldFill.INSERT 插入时填充
  28. FieldFill.UPDATE 更新时填充
  29. FieldFill.INSERT_UPDATE 插入、更新时填充。
  30. @TableLogic
  31. @TableLogic 用于定义表的字段进行逻辑删除(非物理删除)
  32. 注:
  33. 常用属性:
  34. value 用于定义未删除时字段的值
  35. delval 用于定义删除时字段的值
  36. @Version 作用于属性上
  37. @Version 用于字段实现乐观锁

用法:

  1. /**
  2. * 用户测试类
  3. */
  4. //指定数据库表名称 user_test1
  5. @TableName(value = "user_test1")
  6. public class UserTest {
  7. /**
  8. * 主键id 设置自增长策略
  9. */
  10. @TableId(value = "id",type = IdType.AUTO)
  11. private int id;
  12. /**
  13. * 用户名称
  14. */
  15. @TableField(value = "username")
  16. private String username;
  17. /**
  18. * 年龄
  19. */
  20. @TableField(value = "age")
  21. private int age;
  22. /**
  23. * 电话
  24. */
  25. @TableField(value = "tel")
  26. private int tel;
  27. /**
  28. * 创建时间
  29. */
  30. @TableField(value = "create_time")
  31. private Date create_time;
  32. /**
  33. * 最后修改时间
  34. */
  35. @TableField(value = "update_time")
  36. private Date update_time;
  37. /**
  38. * 版本号(用于乐观锁,默认为1)
  39. * @return
  40. */
  41. //@Version
  42. @TableField(value = "version")
  43. private int version;
  44. }

BaseMapper方法简介:

  1. 复制代码
  2. 【添加数据:(增)】
  3. int insert(T entity); // 插入一条记录
  4. 注:
  5. T 表示任意实体类型
  6. entity 表示实体对象
  7. 【删除数据:(删)】
  8. int deleteById(Serializable id); // 根据主键 ID 删除
  9. int deleteByMap(@Param(Constants.COLUMN_MAP) Map<String, Object> columnMap); // 根据 map 定义字段的条件删除
  10. int delete(@Param(Constants.WRAPPER) Wrapper<T> wrapper); // 根据实体类定义的 条件删除对象
  11. int deleteBatchIds(@Param(Constants.COLLECTION) Collection<? extends Serializable> idList); // 进行批量删除
  12. 注:
  13. id 表示 主键 ID
  14. columnMap 表示表字段的 map 对象
  15. wrapper 表示实体对象封装操作类,可以为 null
  16. idList 表示 主键 ID 集合(列表、数组),不能为 null empty
  17. 【修改数据:(改)】
  18. int updateById(@Param(Constants.ENTITY) T entity); // 根据 ID 修改实体对象。
  19. int update(@Param(Constants.ENTITY) T entity, @Param(Constants.WRAPPER) Wrapper<T> updateWrapper); // 根据 updateWrapper 条件修改实体对象
  20. 注:
  21. update 中的 entity set 条件,可以为 null
  22. updateWrapper 表示实体对象封装操作类(可以为 null,里面的 entity 用于生成 where 语句)
  23. 【查询数据:(查)】
  24. T selectById(Serializable id); // 根据 主键 ID 查询数据
  25. List<T> selectBatchIds(@Param(Constants.COLLECTION) Collection<? extends Serializable> idList); // 进行批量查询
  26. List<T> selectByMap(@Param(Constants.COLUMN_MAP) Map<String, Object> columnMap); // 根据表字段条件查询
  27. T selectOne(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper); // 根据实体类封装对象 查询一条记录
  28. Integer selectCount(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper); // 查询记录的总条数
  29. List<T> selectList(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper); // 查询所有记录(返回 entity 集合)
  30. List<Map<String, Object>> selectMaps(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper); // 查询所有记录(返回 map 集合)
  31. List<Object> selectObjs(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper); // 查询所有记录(但只保存第一个字段的值)
  32. <E extends IPage<T>> E selectPage(E page, @Param(Constants.WRAPPER) Wrapper<T> queryWrapper); // 查询所有记录(返回 entity 集合),分页
  33. <E extends IPage<Map<String, Object>>> E selectMapsPage(E page, @Param(Constants.WRAPPER) Wrapper<T> queryWrapper); // 查询所有记录(返回 map 集合),分页
  34. 注:
  35. queryWrapper 表示实体对象封装操作类(可以为 null
  36. page 表示分页查询条件

Service层接口介绍:

IService 内部进一步封装了 BaseMapper 接口的方法(当然也提供了更详细的方法)。
使用时,可以通过 生成的 mapper 类进行 CRUD 操作,也可以通过 生成的 service 的实现类进行 CRUD 操作。(当然,自定义代码执行也可,不选择继承IService)

  1. public interface UserTestService extends IService<UserTest> {
  2. List<UserTest> LikeListUser(String username);
  3. }

IService方法介绍:
  1. 【添加数据:(增)】
  2. default boolean save(T entity); // 调用 BaseMapper 的 insert 方法,用于添加一条数据。
  3. boolean saveBatch(Collection<T> entityList, int batchSize); // 批量插入数据
  4. 注:
  5. entityList 表示实体对象集合
  6. batchSize 表示一次批量插入的数据量,默认为 1000
  7. 【添加或修改数据:(增或改)】
  8. boolean saveOrUpdate(T entity); // id 若存在,则修改, id 不存在则新增数据
  9. default boolean saveOrUpdate(T entity, Wrapper<T> updateWrapper); // 先根据条件尝试更新,然后再执行 saveOrUpdate 操作
  10. boolean saveOrUpdateBatch(Collection<T> entityList, int batchSize); // 批量插入并修改数据
  11. 【删除数据:(删)】
  12. default boolean removeById(Serializable id); // 调用 BaseMapper 的 deleteById 方法,根据 id 删除数据。
  13. default boolean removeByMap(Map<String, Object> columnMap); // 调用 BaseMapper 的 deleteByMap 方法,根据 map 定义字段的条件删除
  14. default boolean remove(Wrapper<T> queryWrapper); // 调用 BaseMapper 的 delete 方法,根据实体类定义的 条件删除对象。
  15. default boolean removeByIds(Collection<? extends Serializable> idList); // 用 BaseMapper 的 deleteBatchIds 方法, 进行批量删除。
  16. 【修改数据:(改)】
  17. default boolean updateById(T entity); // 调用 BaseMapper 的 updateById 方法,根据 ID 选择修改。
  18. default boolean update(T entity, Wrapper<T> updateWrapper); // 调用 BaseMapper 的 update 方法,根据 updateWrapper 条件修改实体对象。
  19. boolean updateBatchById(Collection<T> entityList, int batchSize); // 批量更新数据
  20. 【查找数据:(查)】
  21. default T getById(Serializable id); // 调用 BaseMapper 的 selectById 方法,根据 主键 ID 返回数据。
  22. default List<T> listByIds(Collection<? extends Serializable> idList); // 调用 BaseMapper 的 selectBatchIds 方法,批量查询数据。
  23. default List<T> listByMap(Map<String, Object> columnMap); // 调用 BaseMapper 的 selectByMap 方法,根据表字段条件查询
  24. default T getOne(Wrapper<T> queryWrapper); // 返回一条记录(实体类保存)。
  25. Map<String, Object> getMap(Wrapper<T> queryWrapper); // 返回一条记录(map 保存)。
  26. default int count(Wrapper<T> queryWrapper); // 根据条件返回 记录数。
  27. default List<T> list(); // 返回所有数据。
  28. default List<T> list(Wrapper<T> queryWrapper); // 调用 BaseMapper 的 selectList 方法,查询所有记录(返回 entity 集合)。
  29. default List<Map<String, Object>> listMaps(Wrapper<T> queryWrapper); // 调用 BaseMapper 的 selectMaps 方法,查询所有记录(返回 map 集合)。
  30. default List<Object> listObjs(); // 返回全部记录,但只返回第一个字段的值。
  31. default <E extends IPage<T>> E page(E page, Wrapper<T> queryWrapper); // 调用 BaseMapper 的 selectPage 方法,分页查询
  32. default <E extends IPage<Map<String, Object>>> E pageMaps(E page, Wrapper<T> queryWrapper); // 调用 BaseMapper 的 selectMapsPage 方法,分页查询
  33. 注:
  34. get 用于返回一条记录。
  35. list 用于返回多条记录。
  36. count 用于返回记录总数。
  37. page 用于分页查询。
  38. 【链式调用:】
  39. default QueryChainWrapper<T> query(); // 普通链式查询
  40. default LambdaQueryChainWrapper<T> lambdaQuery(); // 支持 Lambda 表达式的修改
  41. default UpdateChainWrapper<T> update(); // 普通链式修改
  42. default LambdaUpdateChainWrapper<T> lambdaUpdate(); // 支持 Lambda 表达式的修改
  43. 注:
  44. query 表示查询
  45. update 表示修改
  46. Lambda 表示内部支持 Lambda 写法。
  47. 形如:
  48. query().eq("column", value).one();
  49. lambdaQuery().eq(Entity::getId, value).list();
  50. update().eq("column", value).remove();
  51. lambdaUpdate().eq(Entity::getId, value).update(entity);

11. springBoot Mybatis多数据源的配置:

11.1 配置 application.yaml:

  1. server:
  2. port: 8090
  3. spring:
  4. datasource:
  5. db1:
  6. jdbc-url: jdbc:mysql://cdb-5gkdgdz8.bj.tencentcdb.com:10170/test_schema
  7. username: root
  8. password: root123456
  9. db2:
  10. jdbc-url: jdbc:mysql://cdb-5gkdgdz8.bj.tencentcdb.com:10170/bak_schema
  11. username: root
  12. password: root123456
  13. # mybatis.config-location=classpath:mybatis-config.xml
  14. #mybatis:
  15. # mapper-locations: classpath*:/mybatis/mapper/*.xml
  16. # type-aliases-package: com.duing.model
  17. # config-location: classpath:mybatis-config.xml
  18. # mybatis.mapper-locations=classpath*:/mappers/*.xml
  19. # mybatis.type-aliases-package=sample.mybatis.domain

11.2 配置数据源:

  1. package com.duing.config;
  2. import org.springframework.boot.context.properties.ConfigurationProperties;
  3. import org.springframework.boot.jdbc.DataSourceBuilder;
  4. import org.springframework.context.annotation.Bean;
  5. import org.springframework.context.annotation.Configuration;
  6. import javax.sql.DataSource;
  7. @Configuration
  8. public class DBConfig {
  9. @Bean(name="db1")
  10. @ConfigurationProperties("spring.datasource.db1")
  11. public DataSource datasource1(){
  12. return DataSourceBuilder.create().build();
  13. }
  14. @Bean(name="db2")
  15. @ConfigurationProperties("spring.datasource.db2")
  16. public DataSource datasource2(){
  17. return DataSourceBuilder.create().build();
  18. }
  19. }

11.3 配置 SqlSessionTemplate:

  1. package com.duing.config;
  2. import org.apache.ibatis.session.SqlSessionFactory;
  3. import org.mybatis.spring.SqlSessionFactoryBean;
  4. import org.mybatis.spring.SqlSessionTemplate;
  5. import org.mybatis.spring.annotation.MapperScan;
  6. import org.springframework.beans.factory.annotation.Autowired;
  7. import org.springframework.beans.factory.annotation.Qualifier;
  8. import org.springframework.context.annotation.Bean;
  9. import org.springframework.context.annotation.Configuration;
  10. import javax.sql.DataSource;
  11. @Configuration
  12. @MapperScan(basePackages = "com.duing.mapper",sqlSessionFactoryRef = "sqlSessionFactory1")
  13. /**
  14. *basePackages = "com.duing.mapper":指定那个mapper包下面使用该数据源
  15. *sqlSessionFactoryRef = "sqlSessionFactory1":代表使用哪一个工厂
  16. */
  17. public class DBOneConfig {
  18. @Autowired
  19. @Qualifier("db1") //第一个数据源
  20. //@Qualifier("db2") //代表第二个数据源
  21. private DataSource dataSource;
  22. @Bean
  23. public SqlSessionFactory sqlSessionFactory1() throws Exception {
  24. SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
  25. factoryBean.setDataSource(dataSource);
  26. return factoryBean.getObject();
  27. }
  28. @Bean
  29. public SqlSessionTemplate sqlSessionTemplate1() throws Exception {
  30. SqlSessionTemplate template = new SqlSessionTemplate(sqlSessionFactory1());
  31. return template;
  32. }
  33. }

12.springboot 事务:

ps:Spring Boot 默认的事务规则是遇到运行异常(RuntimeException)和程序错误(Error)才会回滚。比如上面我们的例子中抛出的 RuntimeException 就没有问题,但是抛出 SQLException 就无法回滚了。针对非运行时异常,如果要进行事务回滚的话,可以在 @Transactional 注解中使用 rollbackFor 属性来指定异常,比如 @Transactional(rollbackFor = Exception.class),这样就没有问题了,所以在实际项目中,一定要指定异常。

12.1 :Xml方式:

image.png
xml的具体内容如下:

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <beans xmlns="http://www.springframework.org/schema/beans"
  3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
  4. xmlns:tx="http://www.springframework.org/schema/tx"
  5. xsi:schemaLocation="
  6. http://www.springframework.org/schema/beans
  7. http://www.springframework.org/schema/beans/spring-beans.xsd
  8. http://www.springframework.org/schema/tx
  9. http://www.springframework.org/schema/tx/spring-tx.xsd
  10. http://www.springframework.org/schema/aop
  11. http://www.springframework.org/schema/aop/spring-aop.xsd">
  12. //配置DataSourceTransactionManager 的管理
  13. <bean id="txManager"
  14. class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
  15. <property name="dataSource" ref="dataSource" ></property>
  16. </bean>
  17. //配置事务
  18. <tx:advice id="txAdvice" transaction-manager="txManager">
  19. //配置在哪些方法可用
  20. <tx:attributes>
  21. <tx:method name="query*" propagation="SUPPORTS" read-only="true" ></tx:method>
  22. <tx:method name="get*" propagation="SUPPORTS" read-only="true" ></tx:method>
  23. <tx:method name="select*" propagation="SUPPORTS" read-only="true" ></tx:method>
  24. <tx:method name="*" propagation="REQUIRED" rollback-for="Exception" ></tx:method>
  25. </tx:attributes>
  26. </tx:advice>
  27. //service 下面的所有方法可用
  28. <aop:config>
  29. <aop:pointcut id="allManagerMethod" expression="execution (* com.test.service.*.*(..))" />
  30. <aop:advisor advice-ref="txAdvice" pointcut-ref="allManagerMethod" order="0"/>
  31. </aop:config>
  32. </beans>

注意:dataSource是直接拿来用的,所以你在加载DataSource对象时候必须命名为dataSource。比如我用的连接池是阿里的druid,这样写:
image.png
ps: 要么方法名用dataSource,要么注解写成@Bean(“dataSource”)

12.2: 在启动类上添加@ImportResource注解,例如:@ImportResource(“classpath:transaction.xml”)

image.png

12.3 如果使用注解的方式(分为两步:):

12.3.1 在pom.xml中添加依赖:

  1. <dependency>
  2. <groupId>org.springframework</groupId>
  3. <artifactId>spring-aspects</artifactId>
  4. </dependency>

12.3.2 配置事务类:

  1. package com.test.aop;
  2. import java.util.Collections;
  3. import java.util.HashMap;
  4. import java.util.Map;
  5. import javax.sql.DataSource;
  6. import org.springframework.aop.aspectj.AspectJExpressionPointcut;
  7. import org.springframework.aop.support.DefaultPointcutAdvisor;
  8. import org.springframework.beans.factory.annotation.Autowired;
  9. import org.springframework.context.annotation.Bean;
  10. import org.springframework.context.annotation.Configuration;
  11. import org.springframework.jdbc.datasource.DataSourceTransactionManager;
  12. import org.springframework.transaction.TransactionDefinition;
  13. import org.springframework.transaction.interceptor.NameMatchTransactionAttributeSource;
  14. import org.springframework.transaction.interceptor.RollbackRuleAttribute;
  15. import org.springframework.transaction.interceptor.RuleBasedTransactionAttribute;
  16. import org.springframework.transaction.interceptor.TransactionAttribute;
  17. import org.springframework.transaction.interceptor.TransactionInterceptor;
  18. import com.test.domain.DqeProfileDefine;
  19. @Configuration
  20. public class TxAnoConfig {
  21. @Autowired
  22. private DataSource dataSource;
  23. @Bean("txManager")
  24. public DataSourceTransactionManager txManager() {
  25. return new DataSourceTransactionManager(dataSource);
  26. }
  27. /*事务拦截器*/
  28. @Bean("txAdvice")
  29. public TransactionInterceptor txAdvice(DataSourceTransactionManager txManager){
  30. NameMatchTransactionAttributeSource source = new NameMatchTransactionAttributeSource();
  31. /*只读事务,不做更新操作*/
  32. RuleBasedTransactionAttribute readOnlyTx = new RuleBasedTransactionAttribute();
  33. readOnlyTx.setReadOnly(true);
  34. readOnlyTx.setPropagationBehavior(TransactionDefinition.PROPAGATION_NOT_SUPPORTED );
  35. /*当前存在事务就使用当前事务,当前不存在事务就创建一个新的事务*/
  36. //RuleBasedTransactionAttribute requiredTx = new RuleBasedTransactionAttribute();
  37. //requiredTx.setRollbackRules(
  38. // Collections.singletonList(new RollbackRuleAttribute(Exception.class)));
  39. //requiredTx.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
  40. RuleBasedTransactionAttribute requiredTx = new RuleBasedTransactionAttribute(TransactionDefinition.PROPAGATION_REQUIRED,
  41. Collections.singletonList(new RollbackRuleAttribute(Exception.class)));
  42. requiredTx.setTimeout(5);
  43. Map<String, TransactionAttribute> txMap = new HashMap<>();
  44. txMap.put("add*", requiredTx);
  45. txMap.put("save*", requiredTx);
  46. txMap.put("insert*", requiredTx);
  47. txMap.put("update*", requiredTx);
  48. txMap.put("delete*", requiredTx);
  49. txMap.put("get*", readOnlyTx);
  50. txMap.put("query*", readOnlyTx);
  51. source.setNameMap( txMap );
  52. return new TransactionInterceptor(txManager ,source) ;
  53. }
  54. /**切面拦截规则 参数会自动从容器中注入*/
  55. @Bean
  56. public DefaultPointcutAdvisor defaultPointcutAdvisor(TransactionInterceptor txAdvice){
  57. DefaultPointcutAdvisor pointcutAdvisor = new DefaultPointcutAdvisor();
  58. pointcutAdvisor.setAdvice(txAdvice);
  59. AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
  60. pointcut.setExpression("execution (* com.test.service.*.*(..))");
  61. pointcutAdvisor.setPointcut(pointcut);
  62. return pointcutAdvisor;
  63. }
  64. }

12.4 事务的基本用法:

  1. @Service
  2. public class UserServiceImpl implements UserService {
  3. @Resource
  4. private UserMapper userMapper;
  5. @Override
  6. @Transactional
  7. public void isertUser2(User user) throws Exception {
  8. // 插入用户信息
  9. userMapper.insertUser(user);
  10. // 手动抛出异常
  11. throw new SQLException("数据库异常");
  12. }
  13. }

ps:Spring Boot 默认的事务规则是遇到运行异常(RuntimeException)和程序错误(Error)才会回滚。比如上面我们的例子中抛出的 RuntimeException 就没有问题,但是抛出 SQLException 就无法回滚了。针对非运行时异常,如果要进行事务回滚的话,可以在 @Transactional 注解中使用 rollbackFor 属性来指定异常,比如 @Transactional(rollbackFor = Exception.class),这样就没有问题了,所以在实际项目中,一定要指定异常。

12.4.1 重点注意: try…catch,所以导致异常被 ”吃“ 掉,事务无法回滚:

示例:

  1. @Service
  2. public class UserServiceImpl implements UserService {
  3. @Resource
  4. private UserMapper userMapper;
  5. @Override
  6. @Transactional(rollbackFor = Exception.class)
  7. public void isertUser3(User user) {
  8. try {
  9. // 插入用户信息
  10. userMapper.insertUser(user);
  11. // 手动抛出异常
  12. throw new SQLException("数据库异常");
  13. } catch (Exception e) {
  14. // 异常处理逻辑
  15. }
  16. }
  17. }

解决呢?直接往上抛,给上一层来处理即可,千万不要在事务中把异常自己 ”吃“ 掉。
12.5 事务的范围(经典大坑):

13.springboot 过滤器(Filter):

自定义Filter 使用Servlet3.0的注解进行配置第三步的@WebFilter就是3.0的注解

  1. 1)启动类里面增加 @ServletComponentScan,进行扫描
  2. 2)新建一个Filter类,implements Filter,并实现对应的接口
  3. 3) @WebFilter 标记一个类为filter,被spring进行扫描
  4. urlPatterns:拦截规则,支持正则
  5. 4)控制chain.doFilter的方法的调用,来实现是否通过放行不放行,web应用resp.sendRedirect("/index.html");场景:权限控制、用户登录(非前端后端分离场景)等

方式一(@WebFileter注解):

启动类:

  1. @SpringBootApplication
  2. @ServletComponentScan
  3. public class SpringbootstudyApplication {
  4. public static void main(String[] args) {
  5. SpringApplication.run(SpringbootstudyApplication.class, args);
  6. }
  7. }

自定义拦截器:

  1. import java.io.IOException;
  2. import javax.servlet.Filter;
  3. import javax.servlet.FilterChain;
  4. import javax.servlet.FilterConfig;
  5. import javax.servlet.ServletException;
  6. import javax.servlet.ServletRequest;
  7. import javax.servlet.ServletResponse;
  8. import javax.servlet.annotation.WebFilter;
  9. import javax.servlet.http.HttpServletRequest;
  10. import javax.servlet.http.HttpServletResponse;
  11. import javax.servlet.http.HttpServletResponseWrapper;
  12. //过滤器拦截路径
  13. @WebFilter(urlPatterns = "/api/*", filterName = "loginFilter")
  14. public class LoginFilter implements Filter{
  15. /**
  16. * 容器加载的时候调用
  17. */
  18. @Override
  19. public void init(FilterConfig filterConfig) throws ServletException {
  20. System.out.println("拦截器进入========拦截器进入========");
  21. }
  22. /**
  23. * 请求被拦截的时候进行调用
  24. */
  25. @Override
  26. public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
  27. System.out.println("拦截中========拦截中========");
  28. HttpServletRequest hrequest = (HttpServletRequest)servletRequest;
  29. HttpServletResponseWrapper wrapper = new HttpServletResponseWrapper((HttpServletResponse) servletResponse);
  30. if(hrequest.getRequestURI().indexOf("/index") != -1 ||
  31. hrequest.getRequestURI().indexOf("/asd") != -1 ||
  32. hrequest.getRequestURI().indexOf("/online") != -1 ||
  33. hrequest.getRequestURI().indexOf("/login") != -1
  34. ) {
  35. filterChain.doFilter(servletRequest, servletResponse);
  36. }else {
  37. wrapper.sendRedirect("/login");
  38. }
  39. }
  40. /**
  41. * 容器被销毁的时候被调用
  42. */
  43. @Override
  44. public void destroy() {
  45. System.out.println("拦截器销毁========拦截器销毁========");
  46. }
  47. }

image.png

方式二(配置Fileterconfig):

1)创建类实现Filter接口;2)注入springboot容器(代码注入 / 通过注解@WebFilter注入)
image.png

14.springboot 拦截器Interceptor:

拦截器的原理很简单,是 AOP 的一种实现,专门拦截对动态资源的后台请求,即拦截对控制层的请求。使用场景比较多的是判断用户是否有权限请求后台,更拔高一层的使用场景也有,比如拦截器可以结合 websocket 一起使用,用来拦截 websocket 请求,然后做相应的处理等等。拦截器不会拦截静态资源,Spring Boot 的默认静态目录为 resources/static,该目录下的静态页面、js、css、图片等等,不会被拦截(也要看如何实现,有些情况也会拦截,我在下文会指出)。

原理: 拦截器(代理模式)的实现基于反射,代理又分静态代理和动态代理,动态代理是拦截器的简单实现。 被访问的目标方法通过代理类(方法)来执行,这样就可以在真正要执行的方法执行前、后做一些处理。
使用场景:读取cookie得到用户信息并将用户对象放入请求、统计日志等。

14.1 定义拦截器实现方式:

1)创建拦截器类实现HandlerInterceptor接口 HandlerInterceptor 接口是所有自定义拦截器或者 Spring Boot 提供的拦截器的鼻祖,所以,首先来了解下该接口。该接口中有三个方法: preHandle(……)、postHandle(……) 和 afterCompletion(……) 。

preHandle(……) 方法:该方法的执行时机是,当某个 url 已经匹配到对应的 Controller 中的某个方法,且在这个方法执行之前。所以 preHandle(……) 方法可以决定是否将请求放行,这是通过返回值来决定的,返回 true 则放行,返回 false 则不会向后执行。
postHandle(……) 方法:该方法的执行时机是,当某个 url 已经匹配到对应的 Controller 中的某个方法,且在执行完了该方法,但是在 DispatcherServlet 视图渲染之前。所以在这个方法中有个 ModelAndView 参数,可以在此做一些修改动作。
afterCompletion(……) 方法:顾名思义,该方法是在整个请求处理完成后(包括视图渲染)执行,这时做一些资源的清理工作,这个方法只有在 preHandle(……) 被成功执行后并且返回 true 才会被执行。

  1. /**
  2. * 自定义拦截器
  3. * @author shengwu ni
  4. * @date 2018/08/03
  5. */
  6. public class MyInterceptor implements HandlerInterceptor {
  7. private static final Logger logger = LoggerFactory.getLogger(MyInterceptor.class);
  8. @Override
  9. public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
  10. HandlerMethod handlerMethod = (HandlerMethod) handler;
  11. Method method = handlerMethod.getMethod();
  12. String methodName = method.getName();
  13. logger.info("====拦截到了方法:{},在该方法执行之前执行====", methodName);
  14. // 返回true才会继续执行,返回false则取消当前请求
  15. return true;
  16. }
  17. @Override
  18. public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
  19. logger.info("执行完方法之后进执行(Controller方法调用之后),但是此时还没进行视图渲染");
  20. }
  21. @Override
  22. public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
  23. logger.info("整个请求都处理完咯,DispatcherServlet也渲染了对应的视图咯,此时我可以做一些清理的工作了");
  24. }
  25. }

2)创建配置类实现WebMvcConfigurer接口(不会拦截静态资源):
image.png

2.2 取消拦截操作:

据上文,如果我要拦截所有 /admin 开头的 url 请求的话,需要在拦截器配置中添加这个前缀,但是在实际项目中,可能会有这种场景出现:某个请求也是 /admin 开头的,但是不能拦截,比如 /admin/login 等等,这样的话又需要去配置。那么,可不可以做成一个类似于开关的东西,哪里不需要拦截,我就在哪里弄个开关上去,做成这种灵活的可插拔的效果呢?

我们可以定义一个注解,该注解专门用来取消拦截操作,如果某个 Controller 中的方法我们不需要拦截掉,即可在该方法上加上我们自定义的注解即可,下面先定义一个注解:

  1. /**
  2. * 该注解用来指定某个方法不用拦截
  3. */
  4. @Target(ElementType.METHOD)
  5. @Retention(RetentionPolicy.RUNTIME)
  6. public @interface UnInterception {
  7. }

然后在 Controller 中的某个方法上添加该注解,在拦截器处理方法中添加该注解取消拦截的逻辑,如下:

  1. @Override
  2. public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
  3. HandlerMethod handlerMethod = (HandlerMethod) handler;
  4. //获取拦截的方法
  5. Method method = handlerMethod.getMethod();
  6. //获取方法名
  7. String methodName = method.getName();
  8. logger.info("====拦截到了方法:{},在该方法执行之前执行====", methodName);
  9. // 通过方法,可以获取该方法上的自定义注解,然后通过注解来判断该方法是否要被拦截
  10. // @UnInterception 是我们自定义的注解
  11. UnInterception unInterception = method.getAnnotation(UnInterception.class);
  12. if (null != unInterception) {
  13. return true;
  14. }
  15. // 返回true才会继续执行,返回false则取消当前请求
  16. return true;
  17. }

14+ SpringBoot监听器:

1)作用:监听器用于监听Web应用程序中某些对象或信息的创建、销毁、增加、修改、删除等动作,然后做出相应的处理。

2)原理:当对象的状态发生变化时,服务器自动调用监听器的方法

3)使用场景:监听器常用于统计在线人数、在线用户、系统加载时的信息初始化等

4)分类:Servlet中监听器有3种类型,如下,二级标题是每一个监听器接口,注意,是接口,因此需要我们根据监听内容的不同选择不同接口进行实现,实现类中接口实现方法实现相应事件触发时我们自己的业务逻辑。

监听ServletContext、Request、Session作用域的创建和销毁
ServletContextListener:监听ServletContext
HttpSessionListener:监听新的Session创建事件
ServletRequestListener:监听ServletRequest的初始化和销毁
监听ServletContext、Request、Session作用域中属性的变化(增加、修改、删除)
ServletContextAttributeListener:监听Servlet上下文参数的变化
HttpSessionAttributeListener:监听HttpSession参数的变化
ServletRequestAttributeListener:监听ServletRequest参数的变化
监听HttpSession中对象状态的改变(被绑定、解除绑定、钝化、活化)
HttpSessionBindingListener:监听HttpSession,并绑定及解除绑定
HttpSessionActivationListener:监听钝化和活动的HttpSession状态改变

14.1 监听器的介绍:

什么是 web 监听器?web 监听器是一种 Servlet 中特殊的类,它们能帮助开发者监听 web 中特定的事件,比如 ServletContext, HttpSession, ServletRequest 的创建和销毁;变量的创建、销毁和修改等。可以在某些动作前后增加处理,实现监控。

14.2 监听器的使用:

web 监听器的使用场景很多,比如监听 servlet 上下文用来初始化一些数据、监听 http session 用来获取当前在线的人数、监听客户端请求的 servlet request 对象来获取用户的访问信息等等。这一节中,我们主要通过这三个实际的使用场景来学习一下 Spring Boot 中监听器的使用。

2.1 监听Servlet上下文对象:

监听 servlet 上下文对象可以用来初始化数据,用于缓存。什么意思呢?我举一个很常见的场景,比如用户在点击某个站点的首页时,一般都会展现出首页的一些信息,而这些信息基本上或者大部分时间都保持不变的,但是这些信息都是来自数据库。如果用户的每次点击,都要从数据库中去获取数据的话,用户量少还可以接受,如果用户量非常大的话,这对数据库也是一笔很大的开销。
针对这种首页数据,大部分都不常更新的话,我们完全可以把它们缓存起来,每次用户点击的时候,我们都直接从缓存中拿,这样既可以提高首页的访问速度,又可以降低服务器的压力。如果做的更加灵活一点,可以再加个定时器,定期的来更新这个首页缓存。就类似与 CSDN 个人博客首页中排名的变化一样。

模拟一下从数据库查询数据:

  1. @Service
  2. public class UserService {
  3. /**
  4. * 获取用户信息
  5. * @return
  6. */
  7. public User getUser() {
  8. // 实际中会根据具体的业务场景,从数据库中查询对应的信息
  9. return new User(1L, "倪升武", "123456");
  10. }
  11. }

然后写一个监听器,实现 ApplicationListener 接口,重写 onApplicationEvent 方法,将 ContextRefreshedEvent 对象传进去。如果我们想在加载或刷新应用上下文时,也重新刷新下我们预加载的资源,就可以通过监听 ContextRefreshedEvent 来做这样的事情。如下:

  1. /**
  2. * 使用ApplicationListener来初始化一些数据到application域中的监听器
  3. * @author shengni ni
  4. * @date 2018/07/05
  5. */
  6. @Component
  7. public class MyServletContextListener implements ApplicationListener<ContextRefreshedEvent> {
  8. @Override
  9. public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {
  10. // 先获取到application上下文
  11. ApplicationContext applicationContext = contextRefreshedEvent.getApplicationContext();
  12. // 获取对应的service
  13. UserService userService = applicationContext.getBean(UserService.class);
  14. User user = userService.getUser();
  15. // 获取application域对象,将查到的信息放到application域中
  16. ServletContext application = applicationContext.getBean(ServletContext.class);
  17. application.setAttribute("user", user);
  18. }
  19. }

正如注释中描述的一样,首先通过 contextRefreshedEvent 来获取 application 上下文,再通过 application 上下文来获取 UserService 这个 bean,项目中可以根据实际业务场景,也可以获取其他的 bean,然后再调用自己的业务代码获取相应的数据,最后存储到 application 域中,这样前端在请求相应数据的时候,我们就可以直接从 application 域中获取信息,减少数据库的压力。

2.2 监听HTTP会话 Session对象:

监听器还有一个比较常用的地方就是用来监听 session 对象,来获取在线用户数量,现在有很多开发者都有自己的网站,监听 session 来获取当前在下用户数量是个很常见的使用场景,下面来介绍一下如何来使用。

  1. /**
  2. * 使用HttpSessionListener统计在线用户数的监听器
  3. * @author shengwu ni
  4. * @date 2018/07/05
  5. */
  6. @Component
  7. public class MyHttpSessionListener implements HttpSessionListener {
  8. private static final Logger logger = LoggerFactory.getLogger(MyHttpSessionListener.class);
  9. /**
  10. * 记录在线的用户数量
  11. */
  12. public Integer count = 0;
  13. @Override
  14. public synchronized void sessionCreated(HttpSessionEvent httpSessionEvent) {
  15. logger.info("新用户上线了");
  16. count++;
  17. httpSessionEvent.getSession().getServletContext().setAttribute("count", count);
  18. }
  19. @Override
  20. public synchronized void sessionDestroyed(HttpSessionEvent httpSessionEvent) {
  21. logger.info("用户下线了");
  22. count--;
  23. httpSessionEvent.getSession().getServletContext().setAttribute("count", count);
  24. }
  25. }

测试方法如下:

  1. @RestController
  2. @RequestMapping("/listener")
  3. public class TestController {
  4. /**
  5. * 获取当前在线人数,该方法有bug
  6. * @param request
  7. * @return
  8. */
  9. @GetMapping("/total")
  10. public String getTotalUser(HttpServletRequest request) {
  11. Integer count = (Integer) request.getSession().getServletContext().getAttribute("count");
  12. return "当前在线人数:" + count;
  13. }
  14. }

如果关闭一个浏览器再打开,理论上应该还是2,但是实际测试却是 3。原因是 session 销毁的方法没有执行(可以在后台控制台观察日志打印情况),当重新打开时,服务器找不到用户原来的 session,于是又重新创建了一个 session,那怎么解决该问题呢?
解决:把当前的sessionId记录到浏览器。
cookie = new Cookie(“JSESSIONID”, URLEncoder.encode(request.getSession().getId(), “utf-8”));

  1. @GetMapping("/total2")
  2. public String getTotalUser(HttpServletRequest request, HttpServletResponse response) {
  3. Cookie cookie;
  4. try {
  5. // 把sessionId记录在浏览器中
  6. cookie = new Cookie("JSESSIONID", URLEncoder.encode(request.getSession().getId(), "utf-8"));
  7. cookie.setPath("/");
  8. //设置cookie有效期为2天,设置长一点
  9. cookie.setMaxAge( 48*60 * 60);
  10. response.addCookie(cookie);
  11. } catch (UnsupportedEncodingException e) {
  12. e.printStackTrace();
  13. }
  14. Integer count = (Integer) request.getSession().getServletContext().getAttribute("count");
  15. return "当前在线人数:" + count;
  16. }

2.3 监听客户端请求Servlet Request对象

  1. /**
  2. * 使用ServletRequestListener获取访问信息
  3. * @author shengwu ni
  4. * @date 2018/07/05
  5. */
  6. @Component
  7. public class MyServletRequestListener implements ServletRequestListener {
  8. private static final Logger logger = LoggerFactory.getLogger(MyServletRequestListener.class);
  9. @Override
  10. public void requestInitialized(ServletRequestEvent servletRequestEvent) {
  11. HttpServletRequest request = (HttpServletRequest) servletRequestEvent.getServletRequest();
  12. logger.info("session id为:{}", request.getRequestedSessionId());
  13. logger.info("request url为:{}", request.getRequestURL());
  14. request.setAttribute("name", "倪升武");
  15. }
  16. @Override
  17. public void requestDestroyed(ServletRequestEvent servletRequestEvent) {
  18. logger.info("request end");
  19. HttpServletRequest request = (HttpServletRequest) servletRequestEvent.getServletRequest();
  20. logger.info("request域中保存的name值为:{}", request.getAttribute("name"));
  21. }
  22. }

14.3 Spring Boot中自定义事件监听:

在实际项目中,我们往往需要自定义一些事件和监听器来满足业务场景,比如在微服务中会有这样的场景:微服务 A 在处理完某个逻辑之后,需要通知微服务 B 去处理另一个逻辑,或者微服务 A 处理完某个逻辑之后,需要将数据同步到微服务 B,这种场景非常普遍,这个时候,我们可以自定义事件以及监听器来监听,一旦监听到微服务 A 中的某事件发生,就去通知微服务 B 处理对应的逻辑。

3.1 自定义事件:

自定义事件需要继承 ApplicationEvent 对象,在事件中定义一个 User 对象来模拟数据,构造方法中将 User 对象传进来初始化。如下:

  1. /**
  2. * 自定义事件
  3. * @author shengwu ni
  4. * @date 2018/07/05
  5. */
  6. public class MyEvent extends ApplicationEvent {
  7. private User user;
  8. public MyEvent(Object source, User user) {
  9. super(source);
  10. this.user = user;
  11. }
  12. // 省去get、set方法
  13. }

3.2 自定义监听器:

接下来,自定义一个监听器来监听上面定义的 MyEvent 事件,自定义监听器需要实现 ApplicationListener 接口即可。如下:

  1. /**
  2. * 自定义监听器,监听MyEvent事件
  3. * @author shengwu ni
  4. * @date 2018/07/05
  5. */
  6. @Component
  7. public class MyEventListener implements ApplicationListener<MyEvent> {
  8. @Override
  9. public void onApplicationEvent(MyEvent myEvent) {
  10. // 把事件中的信息获取到
  11. User user = myEvent.getUser();
  12. // 处理事件,实际项目中可以通知别的微服务或者处理其他逻辑等等
  13. System.out.println("用户名:" + user.getUsername());
  14. System.out.println("密码:" + user.getPassword());
  15. }
  16. }

然后重写 onApplicationEvent 方法,将自定义的 MyEvent 事件传进来,因为该事件中,我们定义了 User 对象(该对象在实际中就是需要处理的数据,在下文来模拟),然后就可以使用该对象的信息了。

3.3 手动发布事件:

这样监听器才能监听到,如下:

  1. /**
  2. * UserService
  3. * @author shengwu ni
  4. */
  5. @Service
  6. public class UserService {
  7. @Resource
  8. private ApplicationContext applicationContext;
  9. /**
  10. * 发布事件
  11. * @return
  12. */
  13. public User getUser2() {
  14. User user = new User(1L, "倪升武", "123456");
  15. // 发布事件
  16. MyEvent event = new MyEvent(this, user);
  17. applicationContext.publishEvent(event);
  18. return user;
  19. }
  20. }

15.springboot Swagger2文档的使用:

15.1 导入swagger相关依赖:

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

15.2 配置 swaggerconfig :

  1. /**
  2. * @Configuration
  3. * 首先声明是配置文件类
  4. * @EnableSwagger2
  5. * 开启swagger功能
  6. */
  7. @Configuration
  8. @EnableSwagger2
  9. public class SwaggerConfig {
  10. /**
  11. * 使用swagger需要创建一个摘要 Docket
  12. * 摘要参数如下:
  13. * 文档类型 - 使用swagger2 - DocumentationType.SWAGGER_2
  14. * 文档通过一系列的选择器组成 api path
  15. * select()设置apis()和paths()
  16. * apis 查找生成哪些controller的接口
  17. * 获取所有RequestHandlerSelectors.any()
  18. * path 在查找出来的接口中进行筛选
  19. * @return
  20. */
  21. @Bean
  22. public Docket createRestApi() {
  23. return new Docket(DocumentationType.SWAGGER_2)
  24. .pathMapping("/")
  25. .select()
  26. .apis(RequestHandlerSelectors.basePackage("com.nvn.controller"))
  27. .paths(PathSelectors.any())
  28. .build().apiInfo(apiInfo());
  29. }
  30. /**
  31. * 自定义文档的介绍
  32. * 通过ApiInfoBuilder创建ApiInfo
  33. * 参数可以设置 title description version 标题 描述 版本等等
  34. * @return
  35. */
  36. private ApiInfo apiInfo(){
  37. return new ApiInfoBuilder()
  38. .title("spring-boot-duing-06-swagger")
  39. .description("这是一个简单的swagger demo。")
  40. .version("1.0")
  41. .build();
  42. }
  43. }

15.3 ps: 输入http://localhost:8080/swagger-ui.html 访问页面:

15.4 对对应的接口书写文档信息:

  1. @RestController
  2. @Api(tags = "用户管理相关接口")
  3. @RequestMapping("/user")
  4. public class UserController {
  5. @PostMapping("/")
  6. @ApiOperation("添加用户的接口")
  7. @ApiImplicitParams({
  8. @ApiImplicitParam(name = "username", value = "用户名", defaultValue = "李四"),
  9. @ApiImplicitParam(name = "address", value = "用户地址", defaultValue = "深圳", required = true)
  10. }
  11. )
  12. public RespBean addUser(String username, @RequestParam(required = true) String address) {
  13. return new RespBean();
  14. }
  15. @GetMapping("/")
  16. @ApiOperation("根据id查询用户的接口")
  17. @ApiImplicitParam(name = "id", value = "用户id", defaultValue = "99", required = true)
  18. public User getUserById(@PathVariable Integer id) {
  19. User user = new User();
  20. user.setId(id);
  21. return user;
  22. }
  23. @PutMapping("/{id}")
  24. @ApiOperation("根据id更新用户的接口")
  25. public User updateUserById(@RequestBody User user) {
  26. return user;
  27. }
  28. }

这里边涉及到多个API说明:
1.@Api注解可以用来标记当前Controller的功能。

2.@ApiOperation注解用来标记一个方法的作用。
3.@ApiImplicitParam注解用来描述一个参数,可以配置参数的中文含义,也可以给参数设置默认值,这样在接口测试的时候可以避免手动输入。

4.如果有多个参数,则需要使用多个@ApiImplicitParam注解来描述,多个@ApiImplicitParam注解需要放在一个@ApiImplicitParams注解中。

5.需要注意的是,@ApiImplicitParam注解中虽然可以指定参数是必填的,但是却不能代替@RequestParam(required = true),前者的必填只是在Swagger2框架内必填,抛弃Swagger2,这个限制就没用了,所以假如开发者需要指定一个参数必填,@RequestParam(required = true)注解还是不能省略。

6.如果参数是一个对象(例如上文的更新接口),对于参数的描述也可以放在实体类中。例如下面一段代码:

  1. @ApiModel
  2. public class User {
  3. @ApiModelProperty(value = "用户id")
  4. private Integer id;
  5. @ApiModelProperty(value = "用户名")
  6. private String username;
  7. @ApiModelProperty(value = "用户地址")
  8. private String address;
  9. //getter/setter
  10. }

image.png

16 SpringBoot异步任务:

16.1 异步方式一:

使用线程池,创建新的线程处理:
contoller:

  1. /**
  2. * 异步处理1:线程池,创建新线程处理
  3. * @return
  4. */
  5. @RequestMapping(value = "test3",method = RequestMethod.GET)
  6. public String test3(){
  7. ExecutorService service = Executors.newFixedThreadPool(5);
  8. RunnableTask1 task1 = new RunnableTask1();
  9. service.execute(task1);
  10. logger.info("=========》当前线程名:"+Thread.currentThread().getName());
  11. return "异步,正在解析......";
  12. }


Service线程任务:

  1. public class RunnableTask1 implements Runnable{
  2. private final Logger logger = LoggerFactory.getLogger(getClass());
  3. @Override
  4. public void run(){
  5. Building building = new Building();
  6. synchronized (building){
  7. try {
  8. for (int i = 1;i <= 100;i++){
  9. System.out.println(Thread.currentThread().getName()+"----------异步:>"+i);
  10. building.wait(200);
  11. }
  12. }catch (Exception ex){
  13. ex.printStackTrace();
  14. }
  15. }
  16. }
  17. }


我们看控制台,会发现,主线程,和处理任务的线程,不是一个线程,也就是,当页面请求后,主线程会返回我们想要返回的标识,这里返回的是一个字符串:异步,正在解析……,而线程池新开了一个线程,在后台处理业务逻辑。所以,此时访问接口后,会立马返回,页面不用等待,处理逻辑在后台默默进行。控制台如下:
20180116094009928.png

16.2 异步方式二:

这种方式,是springBoot自身的一种异步方式,使用注解实现,非常方便,我们在想要异步执行的方法上加上@Async注解,在controller上加上@EnableAsync,即可。注意,这里的异步方法,只能在自身之外调用,在本类调用是无效的。
controller:

  1. @RestController
  2. @RequestMapping("tmall")
  3. @EnableAsync
  4. public class LoginController {
  5. private final org.slf4j.Logger logger = LoggerFactory.getLogger(getClass());
  6. @Autowired
  7. private LoginService loginService;
  8. /**
  9. * 异步处理2:使用springBoot自带async注解
  10. */
  11. @RequestMapping(value = "test1",method = RequestMethod.GET)
  12. public String test1(){
  13. loginService.getTest1();
  14. logger.info("============>"+Thread.currentThread().getName());
  15. return "异步,正在解析......";
  16. }

serviceImpl:

  1. /**异步方法
  2. * 有@Async注解的方法,默认就是异步执行的,会在默认的线程池中执行,但是此方法不能在本类调用;启动类需添加直接开启异步执行@EnableAsync。
  3. * */
  4. @Async
  5. @Override
  6. public String getTest1(){
  7. Building building = new Building();
  8. synchronized (building){
  9. try {
  10. for (int i = 1;i <= 100;i++){
  11. logger.info(Thread.currentThread().getName()+"----------异步:>"+i);
  12. building.wait(200);
  13. }
  14. return "执行异步任务完毕";
  15. }catch (Exception ex){
  16. ex.printStackTrace();
  17. }
  18. }
  19. return Thread.currentThread().getName()+"执行完毕";
  20. }

看控制台,会发现,页面发出请求后,主线程会返回,而内置的线程池会新开线程,在后台执行任务。此时页面不用等待,可以继续其他操作。
20180116094822858.png

可以看到,很多情况下,异步处理,是一种很常见,而且很高效的方式,我比较喜欢使用springBoot自带的注解方式,只用两个注解即可了。

17 SpringBoot Mail邮件任务:

17.1 导入依赖:

spring-boot-starter-mail:这个启动器内部封装了发送邮件的代码,必须引入的。
spring-boot-starter-web:这个启动器整合了JavaWeb常用的功能,如果只是简单在测试中发送邮件,那么就不需要引入它。
spring-boot-starter-thymeleaf:模板引擎依赖,如果需要将数据渲染到页面上,就需要引入它。

  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-thymeleaf</artifactId>
  8. </dependency>
  9. <dependency>
  10. <groupId>org.springframework.boot</groupId>
  11. <artifactId>spring-boot-starter-web</artifactId>
  12. </dependency>

17.2 配置发送方信息(application.yml):

我们需要在SpringBoot配置文件(application.yml)中配置发送方的信息,包括以下信息:
用户名:也就是我们的邮箱账号
密码:我们刚刚生成的授权码
邮箱主机:配置使用哪个邮箱服务器发送邮件
还有等等其他可选配置,在下面配置代码中,大家需要填写自己的用户名、密码、from(和用户名一样)。

  1. spring:
  2. mail:
  3. host: smtp.qq.com #发送邮件服务器
  4. username: #发送邮件的邮箱地址xxx@qq.com
  5. password: #客户端授权码,不是邮箱密码,这个在qq邮箱设置里面自动生成的
  6. properties.mail.smtp.port: 465 #端口号465或587
  7. from: # 发送邮件的地址,和上面username一致
  8. properties.mail.smtp.starttls.enable: true
  9. properties.mail.smtp.starttls.required: true
  10. properties.mail.smtp.ssl.enable: true
  11. default-encoding: utf-8

17.3 编写EmailService业务类:

我们需要自己封装邮件服务,这个服务只是便于Controller层调用,也就是根据客户端发送的请求来调用,封装三种邮件服务

  1. 发送普通文本邮件
  2. 发送HTML邮件:一般在注册激活时使用
  3. 发送带附件的邮件:可以发送图片等等附件 ```java public interface EmailService {

/**

  • 发送文本邮件
  • @param to 收件人
  • @param subject 主题
  • @param content 内容 */ void sendSimpleMail(String to, String subject, String content);

/**

  • 发送HTML邮件
  • @param to 收件人
  • @param subject 主题
  • @param content 内容 */ void sendHtmlMail(String to, String subject, String content);

/**

  • 发送带附件的邮件
  • @param to 收件人
  • @param subject 主题
  • @param content 内容
  • @param filePath 附件 */ public void sendAttachmentsMail(String to, String subject, String content, String filePath); }
  1. **EmailServiceImpl:**
  2. ```java
  3. @Service
  4. public class EmailServiceImpl implements EmailService {
  5. private final Logger logger = LoggerFactory.getLogger(this.getClass());
  6. @Autowired
  7. private JavaMailSender javaMailSender;
  8. //注入配置文件中配置的信息——>from
  9. @Value("${spring.mail.from}")
  10. private String from;
  11. @Override
  12. public void sendSimpleMail(String to, String subject, String content) {
  13. SimpleMailMessage message = new SimpleMailMessage();
  14. //发件人
  15. message.setFrom(from);
  16. //收件人
  17. message.setTo(to);
  18. //邮件主题
  19. message.setSubject(subject);
  20. //邮件内容
  21. message.setText(content);
  22. //发送邮件
  23. javaMailSender.send(message);
  24. }
  25. @Override
  26. public void sendHtmlMail(String to, String subject, String content) {
  27. MimeMessage message = javaMailSender.createMimeMessage();
  28. MimeMessageHelper messageHelper;
  29. try {
  30. messageHelper = new MimeMessageHelper(message,true);
  31. messageHelper.setFrom(from);
  32. messageHelper.setTo(to);
  33. message.setSubject(subject);
  34. messageHelper.setText(content,true);
  35. javaMailSender.send(message);
  36. logger.info("邮件已经发送!");
  37. } catch (MessagingException e) {
  38. logger.error("发送邮件时发生异常:"+e);
  39. }
  40. }
  41. @Override
  42. public void sendAttachmentsMail(String to, String subject, String content, String filePath) {
  43. MimeMessage message = javaMailSender.createMimeMessage();
  44. MimeMessageHelper messageHelper;
  45. try {
  46. messageHelper = new MimeMessageHelper(message,true);
  47. messageHelper.setFrom(from);
  48. messageHelper.setTo(to);
  49. messageHelper.setSubject(subject);
  50. messageHelper.setText(content,true);
  51. //携带附件
  52. FileSystemResource file = new FileSystemResource(filePath);
  53. String fileName = filePath.substring(filePath.lastIndexOf(File.separator));
  54. messageHelper.addAttachment(fileName,file);
  55. javaMailSender.send(message);
  56. logger.info("邮件加附件发送成功!");
  57. } catch (MessagingException e) {
  58. logger.error("发送失败:"+e);
  59. }
  60. }
  61. }

EmailServiceTest:

  1. @RunWith(SpringRunner.class)
  2. @SpringBootTest
  3. public class EmailServiceTest{
  4. @Autowired
  5. private EmailService emailService;
  6. @Test
  7. public void sendSimpleEmail(){
  8. String content = "你好,恭喜你...";
  9. emailService.sendSimpleMail("XXX@qq.com","祝福邮件",content);
  10. }
  11. @Test
  12. public void sendMimeEmail(){
  13. String content = "<a href='https://blog.csdn.net/'>你好,欢迎注册网站,请点击链接激活</a>";
  14. emailService.sendHtmlMail("XXX@163.com","激活邮件",content);
  15. }
  16. @Test
  17. public void sendAttachment(){
  18. emailService.sendAttachmentsMail("XX@qq.com","发送附件","这是Java体系图","F:/图片/爱旅行.jpg");
  19. }
  20. }

18 SpringBoot参数校验:

19 SpringBoot全局异常处理:

在项目开发过程中,不管是对底层数据库的操作过程,还是业务层的处理过程,还是控制层的处理过程,都不可避免会遇到各种可预知的、不可预知的异常需要处理。如果对每个过程都单独作异常处理,那系统的代码耦合度会变得很高,此外,开发工作量也会加大而且不好统一,这也增加了代码的维护成本。
针对这种实际情况,我们需要将所有类型的异常处理从各处理过程解耦出来,这样既保证了相关处理过程的功能单一,也实现了异常信息的统一处理和维护。同时,我们也不希望直接把异常抛给用户,应该对异常进行处理,对错误信息进行封装,然后返回一个友好的信息给用户。这节主要总结一下项目中如何使用 Spring Boot 如何拦截并处理全局的异常。

19.1 定义返回的统一 json 结构:

前端或者其他服务请求本服务的接口时,该接口需要返回对应的 json 数据,一般该服务只需要返回请求着需要的参数即可,但是在实际项目中,我们需要封装更多的信息,比如状态码 code、相关信息 msg 等等,这一方面是在项目中可以有个统一的返回结构,整个项目组都适用,另一方面是方便结合全局异常处理信息,因为异常处理信息中一般我们需要把状态码和异常内容反馈给调用方。

  1. public class JsonResult {
  2. /**
  3. * 异常码
  4. */
  5. protected String code;
  6. /**
  7. * 异常信息
  8. */
  9. protected String msg;
  10. public JsonResult() {
  11. this.code = "200";
  12. this.msg = "操作成功";
  13. }
  14. public JsonResult(String code, String msg) {
  15. this.code = code;
  16. this.msg = msg;
  17. }
  18. // get set
  19. }

19.2 处理系统异常:

新建一个 GlobalExceptionHandler 全局异常处理类,然后加上 @ControllerAdvice 注解即可拦截项目中抛出的异常,如下:

  1. @ControllerAdvice
  2. @ResponseBody
  3. public class GlobalExceptionHandler {
  4. // 打印log
  5. private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
  6. // ……
  7. }

我们点开 @ControllerAdvice 注解可以看到,@ControllerAdvice 注解包含了 @Component 注解,说明在 Spring Boot 启动时,也会把该类作为组件交给 Spring 来管理。除此之外,该注解还有个 basePackages 属性,该属性是用来拦截哪个包中的异常信息,一般我们不指定这个属性,我们拦截项目工程中的所有异常。@ResponseBody 注解是为了异常处理完之后给调用方输出一个 json 格式的封装数据。

19.3 处理参数缺失异常:

在前后端分离的架构中,前端请求后台的接口都是通过 rest 风格来调用,有时候,比如 POST 请求 需要携带一些参数,但是往往有时候参数会漏掉。另外,在微服务架构中,涉及到多个微服务之间的接口调用时,也可能出现这种情况,此时我们需要定义一个处理参数缺失异常的方法,来给前端或者调用方提示一个友好信息。
参数缺失的时候,会抛出 HttpMessageNotReadableException,我们可以拦截该异常,做一个友好处理,如下:

  1. /**
  2. * 缺少请求参数异常
  3. * @param ex HttpMessageNotReadableException
  4. * @return
  5. */
  6. @ExceptionHandler(MissingServletRequestParameterException.class)
  7. @ResponseStatus(value = HttpStatus.BAD_REQUEST)
  8. public JsonResult handleHttpMessageNotReadableException(
  9. MissingServletRequestParameterException ex) {
  10. logger.error("缺少请求参数,{}", ex.getMessage());
  11. return new JsonResult("400", "缺少必要的请求参数");
  12. }
  1. @RestController
  2. @RequestMapping("/exception")
  3. public class ExceptionController {
  4. private static final Logger logger = LoggerFactory.getLogger(ExceptionController.class);
  5. @PostMapping("/test")
  6. public JsonResult test(@RequestParam("name") String name,
  7. @RequestParam("pass") String pass) {
  8. logger.info("name:{}", name);
  9. logger.info("pass:{}", pass);
  10. return new JsonResult();
  11. }
  12. }

image.png

2.2 处理空指针异常:

空指针异常是开发中司空见惯的东西了,一般发生的地方有哪些呢?
先来聊一聊一些注意的地方,比如在微服务中,经常会调用其他服务获取数据,这个数据主要是 json 格式的,但是在解析 json 的过程中,可能会有空出现,所以我们在获取某个 jsonObject 时,再通过该 jsonObject 去获取相关信息时,应该要先做非空判断。
还有一个很常见的地方就是从数据库中查询的数据,不管是查询一条记录封装在某个对象中,还是查询多条记录封装在一个 List 中,我们接下来都要去处理数据,那么就有可能出现空指针异常,因为谁也不能保证从数据库中查出来的东西就一定不为空,所以在使用数据时一定要先做非空判断。
对空指针异常的处理很简单,和上面的逻辑一样,将异常信息换掉即可。如下:

  1. @ControllerAdvice
  2. @ResponseBody
  3. public class GlobalExceptionHandler {
  4. private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
  5. /**
  6. * 空指针异常
  7. * @param ex NullPointerException
  8. * @return
  9. */
  10. @ExceptionHandler(NullPointerException.class)
  11. @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
  12. public JsonResult handleTypeMismatchException(NullPointerException ex) {
  13. logger.error("空指针异常,{}", ex.getMessage());
  14. return new JsonResult("500", "空指针异常了");
  15. }
  16. }

19.4 拦截exception异常:

当然了,异常很多,比如还有 RuntimeException,数据库还有一些查询或者操作异常等等。由于 Exception 异常是父类,所有异常都会继承该异常,所以我们可以直接拦截 Exception 异常,一劳永逸:

  1. @ControllerAdvice
  2. @ResponseBody
  3. public class GlobalExceptionHandler {
  4. private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
  5. /**
  6. * 系统异常 预期以外异常
  7. * @param ex
  8. * @return
  9. */
  10. @ExceptionHandler(Exception.class)
  11. @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
  12. public JsonResult handleUnexpectedServer(Exception ex) {
  13. logger.error("系统异常:", ex);
  14. return new JsonResult("500", "系统发生异常,请联系管理员");
  15. }
  16. }

19.5 拦截自定义异常:

19.5.1 定义异常信息:

由于在业务中,有很多异常,针对不同的业务,可能给出的提示信息不同,所以为了方便项目异常信息管理,我们一般会定义一个异常信息枚举类。比如:

  1. /**
  2. * 业务异常提示信息枚举类
  3. * @author shengwu ni
  4. */
  5. public enum BusinessMsgEnum {
  6. /** 参数异常 */
  7. PARMETER_EXCEPTION("102", "参数异常!"),
  8. /** 等待超时 */
  9. SERVICE_TIME_OUT("103", "服务调用超时!"),
  10. /** 参数过大 */
  11. PARMETER_BIG_EXCEPTION("102", "输入的图片数量不能超过50张!"),
  12. /** 500 : 一劳永逸的提示也可以在这定义 */
  13. UNEXPECTED_EXCEPTION("500", "系统发生异常,请联系管理员!");
  14. // 还可以定义更多的业务异常
  15. /**
  16. * 消息码
  17. */
  18. private String code;
  19. /**
  20. * 消息内容
  21. */
  22. private String msg;
  23. private BusinessMsgEnum(String code, String msg) {
  24. this.code = code;
  25. this.msg = msg;
  26. }
  27. // set get方法
  28. }

19.5.2 拦截自定义异常:

然后我们可以定义一个业务异常,当出现业务异常时,我们就抛这个自定义的业务异常即可。比如我们定义一个 BusinessErrorException 异常,如下:

  1. /**
  2. * 自定义业务异常
  3. * @author shengwu ni
  4. */
  5. public class BusinessErrorException extends RuntimeException {
  6. private static final long serialVersionUID = -7480022450501760611L;
  7. /**
  8. * 异常码
  9. */
  10. private String code;
  11. /**
  12. * 异常提示信息
  13. */
  14. private String message;
  15. public BusinessErrorException(BusinessMsgEnum businessMsgEnum) {
  16. this.code = businessMsgEnum.code();
  17. this.message = businessMsgEnum.msg();
  18. }
  19. // get set方法
  20. }

在构造方法中,传入我们上面自定义的异常枚举类,所以在项目中,如果有新的异常信息需要添加,我们直接在枚举类中添加即可,很方便,做到统一维护,然后再拦截该异常时获取即可。

  1. @ControllerAdvice
  2. @ResponseBody
  3. public class GlobalExceptionHandler {
  4. private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
  5. /**
  6. * 拦截业务异常,返回业务异常信息
  7. * @param ex
  8. * @return
  9. */
  10. @ExceptionHandler(BusinessErrorException.class)
  11. @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
  12. public JsonResult handleBusinessError(BusinessErrorException ex) {
  13. String code = ex.getCode();
  14. String message = ex.getMessage();
  15. return new JsonResult(code, message);
  16. }
  17. }

20 SpringBoot中的切面AOP的处理:

20.1 Spring Boot 中的 AOP 处理:

20.1.1 AOP 依赖:

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

20.1.2 实现 AOP 切面:

Spring Boot 中使用 AOP 非常简单,假如我们要在项目中打印一些 log,在引入了上面的依赖之后,我们新建一个类 LogAspectHandler,用来定义切面和处理方法。只要在类上加个@Aspect注解即可。@Aspect 注解用来描述一个切面类,定义切面类的时候需要打上这个注解。@Component 注解让该类交给 Spring 来管理。

  1. @Aspect
  2. @Component
  3. public class LogAspectHandler {
  4. }

这里主要介绍几个常用的注解及使用:

1.@Pointcut:定义一个切面,即上面所描述的关注的某件事入口。
2.@Before:在做某件事之前做的事。
3.@After:在做某件事之后做的事。
4.@AfterReturning:在做某件事之后,对其返回值做增强处理。
5.@AfterThrowing:在做某件事抛出异常时,处理。

20.1.2.1 @Pointcut 注解:

@Pointcut 注解:用来定义一个切面(切入点),即上文中所关注的某件事情的入口。切入点决定了连接点关注的内容,使得我们可以控制通知什么时候执行。

  1. @Aspect
  2. @Component
  3. public class LogAspectHandler {
  4. /**
  5. * 定义一个切面,拦截com.itcodai.course09.controller包和子包下的所有方法
  6. */
  7. @Pointcut("execution(* com.itcodai.course09.controller..*.*(..))")
  8. public void pointCut() {}
  9. }

@Pointcut 注解指定一个切面,定义需要拦截的东西,这里介绍两个常用的表达式:一个是使用 execution(),另一个是使用 annotation()。
以 execution( com.itcodai.course09.controller...*(..))) 表达式为例,语法如下:

execution() 为表达式主体
第一个 号的位置:表示返回值类型, 表示所有类型
包名:表示需要拦截的包名,后面的两个句点表示当前包和当前包的所有子包,com.itcodai.course09.controller 包、子包下所有类的方法
第二个 号的位置:表示类名, 表示所有类
(..) :这个星号表示方法名, 表示所有的方法,后面括弧里面表示方法的参数,两个句点表示任何参数

annotation() 方式是针对某个注解来定义切面,比如我们对具有@GetMapping注解的方法做切面,可以如下定义切面:

  1. @Pointcut("@annotation(org.springframework.web.bind.annotation.GetMapping)")
  2. public void annotationCut() {}

然后使用该切面的话,就会切入注解是 @GetMapping 的方法。因为在实际项目中,可能对于不同的注解有不同的逻辑处理,比如 @GetMapping、@PostMapping、@DeleteMapping 等。所以这种按照注解的切入方式在实际项目中也很常用。

20.1.3 切面类的编写:

  1. @Aspect
  2. @Component
  3. public class LogAspectHandler {
  4. private final Logger logger = LoggerFactory.getLogger(this.getClass());
  5. /**
  6. * 定义一个切面,拦截com.itcodai.course09.controller包下的所有方法
  7. */
  8. @Pointcut("execution(* com.itcodai.course09.controller..*.*(..))")
  9. public void pointCut() {}
  10. /**
  11. * 在上面定义的切面方法之前执行该方法
  12. * @param joinPoint jointPoint
  13. */
  14. @Before("pointCut()")
  15. public void doBefore(JoinPoint joinPoint) {
  16. logger.info("====doBefore方法进入了====");
  17. // 获取签名
  18. Signature signature = joinPoint.getSignature();
  19. // 获取切入的包名
  20. String declaringTypeName = signature.getDeclaringTypeName();
  21. // 获取即将执行的方法名
  22. String funcName = signature.getName();
  23. logger.info("即将执行方法为: {},属于{}包", funcName, declaringTypeName);
  24. // 也可以用来记录一些信息,比如获取请求的url和ip
  25. ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
  26. HttpServletRequest request = attributes.getRequest();
  27. // 获取请求url
  28. String url = request.getRequestURL().toString();
  29. // 获取请求ip
  30. String ip = request.getRemoteAddr();
  31. logger.info("用户请求的url为:{},ip地址为:{}", url, ip);
  32. }
  33. /**
  34. * 在上面定义的切面方法之后执行该方法
  35. * @param joinPoint jointPoint
  36. */
  37. @After("pointCut()")
  38. public void doAfter(JoinPoint joinPoint) {
  39. logger.info("====doAfter方法进入了====");
  40. Signature signature = joinPoint.getSignature();
  41. String method = signature.getName();
  42. logger.info("方法{}已经执行完", method);
  43. }
  44. /**
  45. * 在上面定义的切面方法返回后执行该方法,可以捕获返回对象或者对返回对象进行增强
  46. * @param joinPoint joinPoint
  47. * @param result result
  48. */
  49. @AfterReturning(pointcut = "pointCut()", returning = "result")
  50. public void doAfterReturning(JoinPoint joinPoint, Object result) {
  51. Signature signature = joinPoint.getSignature();
  52. String classMethod = signature.getName();
  53. logger.info("方法{}执行完毕,返回参数为:{}", classMethod, result);
  54. // 实际项目中可以根据业务做具体的返回值增强
  55. logger.info("对返回参数进行业务上的增强:{}", result + "增强版");
  56. }
  57. /**
  58. * 在上面定义的切面方法执行抛异常时,执行该方法
  59. * @param joinPoint jointPoint
  60. * @param ex ex
  61. */
  62. @AfterThrowing(pointcut = "pointCut()", throwing = "ex")
  63. public void afterThrowing(JoinPoint joinPoint, Throwable ex) {
  64. Signature signature = joinPoint.getSignature();
  65. String method = signature.getName();
  66. // 处理异常的逻辑
  67. logger.info("执行方法{}出错,异常为:{}", method, ex);
  68. }
  69. }