Spring Boot 总结
欢迎光临我的博客[http://poetize.cn],前端使用Vue2,聊天室使用Vue3,后台使用Spring Boot
Spring Boot 优点
快速创建与框架集成
内嵌Servlet容器
starters自动依赖与版本控制
自动配置
运行时应用监控
Spring Boot 自动配置
@SpringBootApplication ->
@EnableAutoConfiguration ->
@AutoConfigurationPackage ->
@Import({Registrar.class}):
扫描启动类所在的包及其子包的注解。
@Import({AutoConfigurationImportSelector.class}):
从类路径下的 META-INF/spring.factories 导入自动配置类
SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.getBeanClassLoader());
所有配置信息所在jar包:
spring-boot-autoconfigure-2.2.4.RELEASE.jar
自动配置:
Spring Boot启动会加载大量的自动配置类(xxxAutoConfiguration)。
给容器中自动配置类添加组件的时候,会从Properties类(xxxProperties)中获取属性。
xxxAutoConfiguration:自动配置类
xxxProperties:封装配置文件中的相关属性
package org.springframework.boot.autoconfigure.thymeleaf;
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties({ThymeleafProperties.class})
@ConditionalOnClass({TemplateMode.class, SpringTemplateEngine.class})
@AutoConfigureAfter({WebMvcAutoConfiguration.class, WebFluxAutoConfiguration.class})
public class ThymeleafAutoConfiguration {}
package org.springframework.boot.autoconfigure.thymeleaf;
@ConfigurationProperties(prefix = "spring.thymeleaf")
public class ThymeleafProperties {
private static final Charset DEFAULT_ENCODING;
public static final String DEFAULT_PREFIX = "classpath:/templates/";
public static final String DEFAULT_SUFFIX = ".html";
private boolean checkTemplate = true;
private boolean checkTemplateLocation = true;
private String prefix = "classpath:/templates/";
private String suffix = ".html";
private String mode = "HTML";
private Charset encoding;
private boolean cache;
private Integer templateResolverOrder;
private String[] viewNames;
private String[] excludedViewNames;
private boolean enableSpringElCompiler;
private boolean renderHiddenMarkersBeforeCheckboxes;
private boolean enabled;
private final ThymeleafProperties.Servlet servlet;
private final ThymeleafProperties.Reactive reactive;
public ThymeleafProperties() {
this.encoding = DEFAULT_ENCODING;
this.cache = true;
this.renderHiddenMarkersBeforeCheckboxes = false;
this.enabled = true;
this.servlet = new ThymeleafProperties.Servlet();
this.reactive = new ThymeleafProperties.Reactive();
}
}
@EnableWebMvc:完全接管SpringMvc配置,取消SpringBoot自动配置。
Spring Boot 国际化配置
spring.messages.basename=message.login(message是classpath下的文件夹,login是国际化配置文件名)
Spring Boot 视图解析器配置
WebMvcConfigurerAdapter -> registry.addViewController("/").setViewName("index");
Spring Boot 指定日期格式
spring.mvc.date-format=yyyy-MM-dd HH:mm
Spring Boot 拦截器配置
public class LoginInterceptor extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
if(request.getSession().getAttribute("user") == null){
response.sendRedirect("/admin/user");
return false;
}
return true;
}
}
SpringBoot配置拦截器路径(SpringBoot已经配置了静态资源映射):
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns("/admin/**")
.excludePathPatterns("/admin/user/login");
}
Spring Boot 获取配置文件的值
@Component
@PropertySource(value = {"classpath: person.properties"}) //加载指定配置文件
@ConfigurationProperties(prefix = "person")
告诉Spring Boot此类中所有属性和配置文件中的相关配置进行绑定
支持JSR303
@Configuration
public class DruidConfig {
@ConfigurationProperties(prefix = "spring.datasource")
@Bean
public DataSource druid(){
return new DruidDataSource();
}
}
@Value("{person.name}")
支持SpEL
Spring Boot 配置文件加载位置
-file:./config/
-file:./
-classpath:/config/
-classpath:/
Spring Boot thymeleaf
指定版本:
<thymeleaf.version>3.0.2.RELEASE</thymeleaf.version>
<thymeleaf-layout-dialect.version>2.1.1</thymeleaf-layout-dialect.version>
只需将HTML页面放在classpath:/templates/,thymeleaf就能自动渲染(默认配置在classpath:/templates/)
导入thymeleaf名称空间:xmlns:th="http://www.thymeleaf.org"
表达式语法:
${}:
底层是ognl表达式
可以调用对象的属性和方法(${person.name},${list.size()})
使用内置的基本对象(${#locale.country})
使用内置的工具对象(${#strings.toString(obj)})
*{}:
和${}功能一样
补充属性:配合th:object="${person}"使用(*{name})
#{}:获取国际化内容(#{home.welcome})
@{}:替换url(@{/login(username=${person.name}, password=${person.password})})
~{}:片段引用表达式
文本操作:
+:字符串拼接
| the name is ${person.name} |:字符串替换
默认值:value ?: defaultvalue
标签内表达式:
[[${}]] -> th:text
[(${})] -> th:utext
抽取片段:
th:fragment="header(n)"
插入:
将公共片段整个插入到声明的标签中 -> th:insert="~{fragment/fragment::header(1)}"(相对于templates目录)
替换:
将声明的标签替换为公共片段 -> th:replace="~{fragment/fragment::header}"(相对于templates目录)
包含:
将公共片段的内容包含进声明的标签中 -> th:include="~{fragment/fragment::header}"(相对于templates目录)
也可以用id选择器(id="header"):
th:insert="~{fragment/fragment::#header}"
thymeleaf 发送Put请求
配置HiddenHttpMethodFilter:SpringBoot已自动配置好
form表单(method="post")中创建一个input项:
<input type="hidden" name="_method" value="put" th:if="${person!=null}"/>
@PutMapping("/person")
thymeleaf 发送Delete请求
配置HiddenHttpMethodFilter:SpringBoot已自动配置好
<button calss="deteteBtn" th:attr="del_url=@{/person/}+${person.id}">删除</button>
<form id="deleteForm" method="post">
<input type="hidden" name="_method" value="delete"/>
</form>
<script>
$(".deteteBtn").click(function(){
$("#deleteForm").attr("action",$(this).attr("del_url")).submit();
})
</script>
@DeleteMapping("/person/{id}")
Spring Boot 定制错误页面
页面能获取的信息:
timestamp
status
error
exception
message
errors(JSR303检验错误)
1. 有模板引擎:在templates文件夹下的error文件夹下创建error.html
2. 没有模板引擎:静态资源文件夹下的error文件夹下创建error.html
定制错误的Json数据:
@ControllerAdvice
public class MyExceptionHandler {
@ExceptionHandler(NotFoundException.class)
public String handleException(Exception e, HttpServletRequest request){
Map<String,Object> map = new HashMap<>();
request.setAttribute("javax.servlet.error.status_code",500);
map.put("code","500");
map.put("message",e.getMessage());
return "forward:/error";
}
}
/error请求会被BasicErrorController处理
//给容器中加入我们自己定义的ErrorAttributes
@Component
public class MyErrorAttributes extends DefaultErrorAttributes {
@Override
public Map<String, Object> getErrorAttributes(RequestAttributes requestAttributes, boolean includeStackTrace) {
Map<String, Object> map = super.getErrorAttributes(requestAttributes, includeStackTrace);
map.put("name","Sara");
return map;
}
}
Spring Boot 定制和修改Servlet容器相关配置
修改配置文件中以server开头的相关配置
server.port=8081
server.context‐path=/crud
Spring Boot Mybatis使用
@Mapper
public interface PersonMapper {
@Select("select * from person where id=#{id}")
public Person getById(Integer id);
@Delete("delete from person where id=#{id}")
public int deleteById(Integer id);
@Options(useGeneratedKeys = true, keyProperty = "id") //自动生成并封装主键
@Insert("insert into person(name) values(#{name})")
public int insert(Person person);
@Update("update person set name=#{name} where id=#{id}")
public int update(Person person);
}
使用MapperScan批量扫描所有的Mapper接口:
@MapperScan(value = "com.mapper")
@SpringBootApplication
使用mapper配置文件:
mybatis:
config‐location: classpath:mybatis/mybatis‐config.xml #指定全局配置文件的位置
mapper‐locations: classpath:mybatis/mapper/*.xml #指定sql映射文件的位置*/
Spring Boot 缓存
CacheManager:
缓存管理器,管理各种缓存(Cache)组件。
Cache:
缓存接口,定义缓存操作。
实现有:RedisCache、EhCacheCache、ConcurrentMapCache(默认缓存实现,数据保存在ConcurrentMap<Object, Object>中)等。
@EnableCaching:
开启基于注解的缓存。
@CacheConfig(value = "person"):
注解在类上,用于缓存公共配置。
@Cacheable(value = {"缓存名:必须配置"}, key = "可以不配置"):
有数据时方法不会被调用。主要针对方法配置,能够根据方法的请求参数对其结果进行缓存。
@CachePut(value = {"缓存名:必须配置"}, key = "可以不配置"):
保证方法总会被调用,并且缓存结果。主要针对缓存更新。
@CacheEvict(value = {"缓存名:必须配置"}, key = "可以不配置", beforeInvocation = true):
清空缓存。
keyGenerator:
缓存数据时key生成策略。
serialize:
缓存数据时value序列化策略。
整合Redis(对象必须序列化):
引入redis的starter后,容器中保存的是RedisCacheManager,默认CacheManager未注入。
RedisCacheManager会创建RedisCache作为缓存组件。
Spring Boot 任务
异步任务:
启动类上注解:@EnableAsync
方法是上注解:@Async
@Configuration
@EnableAsync
public class ThreadPoolTaskConfig {
/**
* 默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,
* 当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中;
* 当队列满了,就继续创建线程,当线程数量大于等于maxPoolSize后,开始使用拒绝策略拒绝
*/
/** 核心线程数(默认线程数) */
private static final int corePoolSize = 20;
/** 最大线程数 */
private static final int maxPoolSize = 100;
/** 允许线程空闲时间(单位:默认为秒) */
private static final int keepAliveTime = 10;
/** 缓冲队列大小 */
private static final int queueCapacity = 200;
/** 线程池名前缀 */
private static final String threadNamePrefix = "Async-Service-";
@Bean("taskExecutor") //bean的名称,默认为首字母小写的方法名
public ThreadPoolTaskExecutor taskExecutor(){
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(corePoolSize);
executor.setMaxPoolSize(maxPoolSize);
executor.setQueueCapacity(queueCapacity);
executor.setKeepAliveSeconds(keepAliveTime);
executor.setThreadNamePrefix(threadNamePrefix);
//线程池对拒绝任务的处理策略
//CallerRunsPolicy:由调用线程(提交任务的线程)处理该任务
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
// 初始化
executor.initialize();
return executor;
}
}
@Async("taskExecutor")
定时任务:
启动类上注解:@EnableScheduling
方法是上注解:@Scheduled(cron = "0 * * * * MON-SAT") //周一到周五每分钟0秒启动一次
邮件任务:
spring.mail.username=1622138424@qq.com
spring.mail.password=授权码
spring.main.host=smtp.qq.com
spring.mail.properties.mail.smtp.ssl.enable=true
使用JavaMailSenderImpl发送邮件
Spring Boot Security
编写配置类:
@EnableWebSecurity
继承WebSecurityConfigurerAdapter
登陆/注销:
HttpSecurity配置登陆、注销功能
Thymeleaf提供的SpringSecurity标签支持:
需要引入thymeleaf-extras-springsecurity4
sec:authentication="name"获得当前用户的用户名
sec:authorize="hasRole('ADMIN')"当前用户必须拥有ADMIN权限时才会显示标签内容
remember me:
表单添加remember-me的checkbox
配置启用remember-me功能
CSRF(Cross-site request forgery)跨站请求伪造:
HttpSecurity启用csrf功能,会为表单添加_csrf的值,提交携带来预防CSRF
Spring Boot JWT(单点登录SSO)
cookie:由服务器生成,发送给浏览器,浏览器把cookie以kv形式保存到某个目录下的文本文件内,下一次请求同一网站时会把该cookie发送给服务器。
session:服务器使用session把用户的信息临时保存在了服务器上,用户离开网站后session会被销毁。
token应该在HTTP的头部发送从而保证了Http请求无状态。
通过设置服务器属性Access-Control-Allow-Origin:*,让服务器能接受到来自所有域的请求。
实现思路:
用户登录校验,校验成功后就返回Token给客户端
客户端收到数据后保存在客户端
客户端每次访问API是携带Token到服务器端
服务器端采用filter过滤器校验。校验成功则返回请求数据,校验失败则返回错误码
JWT请求流程
用户使用账号发出post请求
服务器使用私钥创建一个jwt
服务器返回这个jwt给浏览器
浏览器将该jwt串在请求头中像服务器发送请求
服务器验证该jwt
返回响应的资源给浏览器
JWT包含了三部分:
Header 头部(标题包含了令牌的元数据,并且包含签名和/或加密算法的类型)
Payload 负载(类似于飞机上承载的物品)
Signature 签名/签证
Header
JWT的头部承载两部分信息:token类型和采用的加密算法。
{
"alg": "HS256",
"typ": "JWT"
}
Payload
载荷就是存放有效信息的地方。
有效信息包含三个部分
标准中注册的声明
公共的声明
私有的声明
标准中注册的声明 (建议但不强制使用) :
iss: jwt签发者
sub: 面向的用户(jwt所面向的用户)
aud: 接收jwt的一方
exp: 过期时间戳(jwt的过期时间,这个过期时间必须要大于签发时间)
nbf: 定义在什么时间之前,该jwt都是不可用的.
iat: jwt的签发时间
jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
公共的声明:
公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密。
私有的声明:
私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。
Signature
jwt的第三部分是一个签证信息
这个部分需要base64加密后的header和base64加密后的payload使用'.'连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。
密钥secret是保存在服务端的,服务端会根据这个密钥进行生成token和进行验证,所以需要保护好。
Spring Boot 和 JWT 的集成
依赖
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.0</version>
</dependency>
需要自定义两个注解
需要登录才能进行操作(UserLoginToken)
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface UserLoginToken {
boolean required() default true;
}
用户Bean
@Data
public class User {
String Id;
String username;
String password;
}
需要写token的生成方法
public String getToken(User user) {
String token="";
token= JWT.create().withAudience(user.getId())
.sign(Algorithm.HMAC256(user.getPassword()));
return token;
}
拦截器:
public class AuthenticationInterceptor implements HandlerInterceptor {
@Autowired
UserService userService;
@Override
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) throws Exception {
//如果不是映射到方法直接通过
if(!(object instanceof HandlerMethod)){
return true;
}
HandlerMethod handlerMethod = (HandlerMethod)object;
Method method = handlerMethod.getMethod();
//检查有没有需要用户权限的注解
if (method.isAnnotationPresent(UserLoginToken.class)) {
UserLoginToken userLoginToken = method.getAnnotation(UserLoginToken.class);
if (userLoginToken.required()) {
// 执行认证
String token = httpServletRequest.getHeader("token");
if (token == null) {
throw new RuntimeException("无token,请重新登录");
}
// 获取 token 中的 user id
String userId;
try {
userId = JWT.decode(token).getAudience().get(0);
} catch (JWTDecodeException j) {
throw new RuntimeException("Token验证失败!");
}
User user = userService.findUserById(userId);
if (user == null) {
throw new RuntimeException("用户不存在,请重新登录");
}
// 验证 token
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(user.getPassword())).build();
try {
jwtVerifier.verify(token);
} catch (JWTVerificationException e) {
throw new RuntimeException("用户密码错误!");
}
}
}
return true;
}
}
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new AuthenticationInterceptor())
.addPathPatterns("/**"); //拦截所有请求,通过判断是否有 @LoginRequired 注解 决定是否需要登录
}
}
Controller
@PostMapping("/login")
public Result login(@RequestBody User user) {
//处理验证,发送Token到前端
...
}
@UserLoginToken
@GetMapping("/getMessage")
public Result getMessage() {
...
}
SimpleDateFormat(线程不安全)
单线程:private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
多线程:
private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>() {
@Override
protected DateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
};
public static Date parse(String dateStr) throws ParseException {
return threadLocal.get().parse(dateStr);
}
public static String format(Date date) {
return threadLocal.get().format(date);
}
private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalDateTime date = LocalDateTime.now()
public static String formatDate(LocalDateTime date) {
return formatter.format(date);
}
public static LocalDateTime parseDate(String date) {
return LocalDateTime.parse(date, formatter);
}
优化 Spring Boot
server:
tomcat:
min-spare-threads: 20
max-threads: 100
connection-timeout: 5000
java -Xms512m -Xmx768m -jar springboot-1.0.jar
Redis + Token机制实现接口幂等性校验
幂等性, 通俗的说就是一个接口, 多次发起同一个请求, 必须保证操作只能执行一次
为需要保证幂等性的每一次请求创建一个唯一标识token, 先获取token, 并将此token存入redis,
请求接口时, 将此token放到header或者作为请求参数请求接口, 后端接口判断redis中是否存在此token:
1. 如果存在,正常处理业务逻辑,并从redis中删除此token,那么,如果是重复请求,由于token已被删除,则不能通过校验,返回请勿重复操作提示
2. 如果不存在,说明参数不合法或者是重复请求,返回提示即可
自定义注解@ApiIdempotent
/**
* 在需要保证 接口幂等性 的Controller的方法上使用此注解
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiIdempotent {}
/**
* 接口幂等性拦截器
*/
public class ApiIdempotentInterceptor implements HandlerInterceptor {
@Autowired
private TokenService tokenService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
if (!(handler instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
ApiIdempotent methodAnnotation = method.getAnnotation(ApiIdempotent.class);
if (methodAnnotation != null) {
//幂等性校验, 校验通过则放行, 校验失败则抛出异常, 并通过统一异常处理返回友好提示
check(request);
}
return true;
}
private void check(HttpServletRequest request) {
tokenService.checkToken(request);
}
}
@Service
public class TokenServiceImpl implements TokenService {
private static final String TOKEN_NAME = "token";
@Autowired
private RdeisTemplate rdeisTemplate;
@Override
public Result createToken() {
...创造Token并放入Redis...
return new Result(...);
}
@Override
public void checkToken(HttpServletRequest request) {
String token = request.getHeader(TOKEN_NAME);
if (StringUtils.isBlank(token)) {//header中不存在token
token = request.getParameter(TOKEN_NAME);
if (StringUtils.isBlank(token)) {//parameter中也不存在token
throw new Exception(...);
}
}
if (...判断Redis中是否有Token...) {
throw new Exception(...);
}
...删除Token并验证是否删除成功(防止多线程问题)...
}
}
/** 需要先获取Token */
@RestController
public class TestController {
@ApiIdempotent
@PostMapping("/")
public Result testIdempotence() {
return new Result();
}
}