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();
    }
}
posted @ 2020-03-27 11:59  LittleDonkey  阅读(498)  评论(0编辑  收藏  举报