SpringBoot实战项目精华总结(四)
一、登录拦截原理与实现 -- AOP
二、微信推送模板消息
三、websocket模板消息推送
四、Freemarker的使用
五、分布式系统下的session及其他
一、登录拦截原理与实现 -- AOP
1.切面类(一个切面方法 - @Pointcut,再写一个具体的实现方法 - @Before)
/**
* @Description 请求验证切面类
* @Date 2020/2/20 11:13
*/
@Aspect
@Component
@Slf4j
public class SellerAuthorizeAspect {
@Autowired
private StringRedisTemplate redisTemplate;
// 先写一个切面方法,切入点为需要验证权限的Controller
@Pointcut("execution(public * com.imooc.controller.Seller*.*(..))" +
"&& !execution(public * com.imooc.controller.SellerUserController.*(..))")
public void verify() {}
// 再写一个切面方法的具体实现
@Before("verify()")
public void doVerify() {
// 重点:这里获取请求request对象
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
//查询cookie
Cookie cookie = CookieUtil.get(request, CookieConstant.TOKEN);
if (cookie == null) {
log.warn("【登录校验】Cookie中查不到token");
throw new SellerAuthorizeException();
}
//去redis里查询
String tokenValue = redisTemplate.opsForValue().get(String.format(RedisConstant.TOKEN_PREFIX, cookie.getValue()));
if (StringUtils.isEmpty(tokenValue)) {
log.warn("【登录校验】Redis中查不到token");
throw new SellerAuthorizeException();
}
}
}
2.不满足验证条件的抛出一个特殊的异常
如:SellerAuthorizeException -- 可以不写内容
3.对特殊异常进行捕获@ControllerAdvice
@ControllerAdvice
public class SellExceptionHandler {
@Autowired
private ProjectUrlConfig projectUrlConfig;
@ExceptionHandler(value = SellerAuthorizeException.class)
public ModelAndView handlerAuthorizeException() {
return new ModelAndView("seller/login");
}
}
二、微信推送模板消息
1.官方文档
2.在微信公众平台设置好模板
3.编写推送给用户订单消息接口(WxMpTemplateData对象可以给关键字设置颜色)
4.可能涉及到IP白名单配置 =>平台=>基本配置=>查看IP白名单
5.可以在yaml文件中设置一个Map,用于存放一系列TemplateId
private Map<String, String> templateId;
6.可以再设置url,让用户点击通知跳转到H5页面;设置miniprogram可跳转到小程序
三、websocket模板消息推送(客户端、服务端都要设置)
1.客户端
基于原生的H5中开发js代码,监听服务端推送来的消息
实现:
- 弹窗代码
<#--弹窗-->
<div class="modal fade" id="myModal" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
<h4 class="modal-title" id="myModalLabel">
提醒
</h4>
</div>
<div class="modal-body">
你有新的订单
</div>
<div class="modal-footer">
<button onclick="javascript:document.getElementById('notice').pause()" type="button"
class="btn btn-default" data-dismiss="modal">关闭
</button>
<button onclick="location.reload()" type="button" class="btn btn-primary">查看新的订单</button>
</div>
</div>
</div>
</div>
- js代码
<script src="https://cdn.bootcss.com/jquery/1.12.4/jquery.min.js"></script>
<script src="https://cdn.bootcss.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
<script>
var websocket = null;
if ('WebSocket' in window) {
websocket = new WebSocket('ws://www.xxx.com/project/webSocket');
} else {
alert('该浏览器不支持websocket!');
}
websocket.onopen = function (event) {
console.log('建立连接');
}
websocket.onclose = function (event) {
console.log('连接关闭');
}
websocket.onmessage = function (event) {
console.log('收到消息:' + event.data)
//弹窗提醒, 播放音乐
$('#myModal').modal('show');
document.getElementById('notice').play();
}
websocket.onerror = function () {
alert('websocket通信发生错误!');
}
window.onbeforeunload = function () {
//在关闭窗口前,关闭ws监听!
websocket.close();
}
</script>
2.后端代码
- 引入springboot依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
- 配置WebSocketConfig
/**
* @Description Websocket配置
*/
@Component
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
- 编写webservice类
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;
import java.util.concurrent.CopyOnWriteArraySet;
/**
* @Description webSocket通信类
* @Date 2020/2/25 13:29
*/
@Component
@ServerEndpoint("/webSocket")
@Slf4j
public class WebSocket {
private Session session; //
private static CopyOnWriteArraySet<WebSocket> webSocketSet = new CopyOnWriteArraySet<>();
@OnOpen
public void onOpen(Session session) {
this.session = session;
webSocketSet.add(this);
log.info("【websocket消息】有新的连接, 总数:{}", webSocketSet.size());
}
@OnClose
public void onClose() {
webSocketSet.remove(this);
log.info("【websocket消息】连接断开, 总数:{}", webSocketSet.size());
}
@OnMessage
public void onMessage(String message) {
log.info("【websocket消息】收到客户端发来的消息:{}", message);
}
public void sendMessage(String message) {
for (WebSocket webSocket : webSocketSet) {
log.info("【websocket消息】广播消息, message={}", message);
try {
webSocket.session.getBasicRemote().sendText(message);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
四、Freemarker的使用
1.分页的页码语法
2.处于某一页时,当前页码不能点
3.上一页、下一页
4.3秒后自动跳转的实现
<script>
setTimeout('location.href="${url}"', 3000);
</script>
5.页面中订单状态的判断
6.公用边栏的抽取实现
6.1 准备一个nav.ftl,在list.ftl引入sidebar
<#--边栏sidebar-->
<#include "../common/nav.ftl">
6.2 同样可以提取head,叫header.ftl
<head>
<meta charset="utf-8">
<title>xxx管理系统</title>
<link href="https://cdn.bootcss.com/bootstrap/3.3.5/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="/sell/css/style.css">
</head>
引入:
<#include "../common/header.ftl">
7.模板页面取不到值报错的解决办法
如果取不到msg字段,就留空值(注意写法)
${msg!""}
如果取不到productInfo对象,就留空值(注意写法)
${(productInfo.productIcon)!''}
8.下拉菜单实现代码示例
<select name="categoryType" class="form-control">
<#list categoryList as category>
<option value="${category.categoryType}"
<#if (productInfo.categoryType)?? && productInfo.categoryType == category.categoryType>
selected
</#if>
>${category.categoryName}
</option>
</#list>
</select>
五、分布式系统下的session
1.分布式系统
三个特点:多节点、消息通信、不共享内存
2.session
->会话控制:保存用户的信息,并在后续的请求中验证身份信息
->session可以理解为一种k-v的机制,session设计的关键点:①.如何设计和获取key,②.如何保存和正确获取对应的value
->sessionId与token:前端请求设置在header中,后端存储在redis中,从而保持会话状态
3.微信网页扫码登录设计
时序:
->访问某个页面时,被前面拦截验证cookie,如果没有openid参数,抛出异常跳转到异常处理类,异常处理类携带一个回调url发起调用微信开发平台,唤起扫码页
->扫码人扫码后,微信回调携带的url?openid=xxx,注意此时的openid与公众平台的不一致
->再验证次openid是否在库,符合条件即设置cookie+缓存登陆成功,否则失败
4.登出
删除redis中存储的k-v,cookie设置失效
5.关于cookie操作的工具类
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;
/**
* @Description cookie工具类
* @Date 2020/2/18 22:20
*/
public class CookieUtil {
/**
* 设置
* @param response
* @param name
* @param value
* @param maxAge
*/
public static void set(HttpServletResponse response,
String name,
String value,
int maxAge) {
Cookie cookie = new Cookie(name, value);
cookie.setPath("/");
cookie.setMaxAge(maxAge);
response.addCookie(cookie);
}
/**
* 获取cookie
* @param request
* @param name
* @return
*/
public static Cookie get(HttpServletRequest request,
String name) {
Map<String, Cookie> cookieMap = readCookieMap(request);
if (cookieMap.containsKey(name)) {
return cookieMap.get(name);
}else {
return null;
}
}
/**
* 将cookie封装成Map
* @param request
* @return
*/
private static Map<String, Cookie> readCookieMap(HttpServletRequest request) {
Map<String, Cookie> cookieMap = new HashMap<>();
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie: cookies) {
cookieMap.put(cookie.getName(), cookie);
}
}
return cookieMap;
}
}
6.代替String拼接的format方法
// 此时%s为占位符,res = "token_xiaoqiang"
String res = String.format("token_%s", “xiaoqiang”);
7.从url中获取请求参数:是value属性!
@RequestParam(value = "page", defaultValue = "1")
表单提交是取的name属性
如果非必传参数,可以设置@RequestParam(value = "page", required = false)
8.枚举通用接口的使用
public interface CodeEnum {
Integer getCode(); // 枚举类共用的方法
}
这样就避免了在每个枚举类中都写一个getByCode的方法
/**
* @Description 根据code+枚举类型,返回枚举工具类
* @Date 2020/2/16 21:54
*/
public class EnumUtil {
public static <T extends CodeEnum> T getByCode(Integer code, Class<T> enumClass) {
for (T each: enumClass.getEnumConstants()) {
if (code.equals(each.getCode())) {
return each;
}
}
return null;
}
}
9.@JsonIgnore可以在返回的json数据中被忽略不返回