1、spring事务管理
1.1 Spring事务管理
声明式事务:
1 通过XML配置,声明某方法的事务特征
2、通过注解,声明某方法的事务特征,注解@Transactional
1.2 @Transactional 注解参数讲解
- 隔离级别
- 传播行为
- 回滚规则
- 是否只读
- 事务超时
传播机制比较难理解,这里要着重说一下:
事务传播行为是为了解决业务层方法之间互相调用的事务问题。
当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。
假如在A方法中,调用了B方法,而B方法是事务操作,则A为B的外部事务。
1.TransactionDefinition.PROPAGATION_REQUIRED
是Spring事务管理中的一种传播行为,它表示如果当前有事务,就沿用当前事务,如果没有,就新建一个事务。这是最常用的传播行为。
优点:可以保证事务的一致性和完整性,也可以避免不必要的事务开启和关闭的开销
缺点:如果事务方法嵌套调用,内部事务和外部事务是同一个事务,内部事务的回滚会导致外部事务也回滚,这可能不是预期的结果。
2、TransactionDefinition.PROPAGATION_REQUIRES_NEW
无论当前是否有事务,都会新建一个事务,并暂停当前事务(如果存在)
优点:可以保证内部事务和外部事务的独立性。内部事务的回滚并不会影响外部事务,也可以避免内部事务受到外部事务的影响
缺点:会增加事务的开启和关闭的开销,也可能导致数据库的竞争和死锁
3、TransactionDefinition.PROPAGATION_NESTED
:
如果当前存在事务,就在嵌套事务内执行;如果当前没有事务,就执行与TransactionDefinition.PROPAGATION_REQUIRED
类似的操作。也就是说:
- 在外部方法开启事务的情况下,在内部开启一个新的事务,作为嵌套事务存在。
- 如果外部方法无事务,则单独开启一个事务,与
PROPAGATION_REQUIRED
类似。
4.TransactionDefinition.PROPAGATION_MANDATORY
如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。(mandatory:强制性)
编程式事务:
通过TransactionTemplate管理事务,并通过它执行数据库的操作
package com.nowcoder.community.service;
import com.nowcoder.community.dao.AlphaDao;
import com.nowcoder.community.dao.DiscussPostMapper;
import com.nowcoder.community.dao.UserMapper;
import com.nowcoder.community.entity.DiscussPost;
import com.nowcoder.community.entity.User;
import com.nowcoder.community.util.CommunityUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Service;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionTemplate;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.Date;
@Service
//@Scope("prototype")
public class AlphaService {
@Autowired
private AlphaDao alphaDao;
@Autowired
private UserMapper userMapper;
@Autowired
private DiscussPostMapper discussPostMapper;
@Autowired
private TransactionTemplate transactionTemplate;
public AlphaService() {
// System.out.println("实例化AlphaService");
}
@PostConstruct
public void init() {
// System.out.println("初始化AlphaService");
}
@PreDestroy
public void destroy() {
// System.out.println("销毁AlphaService");
}
public String find() {
return alphaDao.select();
}
// REQUIRED: 支持当前事务(外部事务),如果不存在则创建新事务.
// REQUIRES_NEW: 创建一个新事务,并且暂停当前事务(外部事务).
// NESTED: 如果当前存在事务(外部事务),则嵌套在该事务中执行(独立的提交和回滚),否则就会REQUIRED一样.
//isolation 隔离性 propagation 传播机制
@Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED)
public Object save1() {
// 新增用户
User user = new User();
user.setUsername("alpha");
user.setSalt(CommunityUtil.generateUUID().substring(0, 5));
user.setPassword(CommunityUtil.md5("123" + user.getSalt()));
user.setEmail("alpha@qq.com");
user.setHeaderUrl("http://image.nowcoder.com/head/99t.png");
user.setCreateTime(new Date());
userMapper.insertUser(user);
// 新增帖子
DiscussPost post = new DiscussPost();
post.setUserId(user.getId());
post.setTitle("Hello");
post.setContent("新人报道!");
post.setCreateTime(new Date());
discussPostMapper.insertDiscussPost(post);
Integer.valueOf("abc");
return "ok";
}
//通过TransactionTemplate进行局部的事务回滚
public Object save2() {
transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
return transactionTemplate.execute(new TransactionCallback<Object>() {
@Override
public Object doInTransaction(TransactionStatus status) {
// 新增用户
User user = new User();
user.setUsername("beta");
user.setSalt(CommunityUtil.generateUUID().substring(0, 5));
user.setPassword(CommunityUtil.md5("123" + user.getSalt()));
user.setEmail("beta@qq.com");
user.setHeaderUrl("http://image.nowcoder.com/head/999t.png");
user.setCreateTime(new Date());
userMapper.insertUser(user);
// 新增帖子
DiscussPost post = new DiscussPost();
post.setUserId(user.getId());
post.setTitle("你好");
post.setContent("我是新人!");
post.setCreateTime(new Date());
discussPostMapper.insertDiscussPost(post);
Integer.valueOf("abc");
return "ok";
}
});
}
}
2、项目中cookie的使用及原理
什么是cookie,即浏览器的一种缓存数据,为了弥补http无状态的缺陷。
对于注册的验证码功能,采用特定的验证码生成工具生成验证码;在这里,项目中并没有采用某一个变量去接收这个验证码的值,而是采用了cookie,即每个客户端都拥有它们自己的cookie,假如采用全局变量去接收,则很容易产生并发的问题;采用cookie即将每个用户都隔离开来,各自用各自浏览器的cookie。
对于登陆状态这个功能,主要就是拿浏览器的cookie去redis中找状态是否过期。同样的,并不会用项目中的某个变量去接收ticket,这样同上会产生并发问题。cookie中存储ticket可以保证不同用户都可以存储它们自己的ticket,并可以拿其去进行验证
cookie的用法如下:
例如,如果要创建一个名为user,值为Tom的cookie,并将其添加到响应中,可以使用以下代码:
Cookie cookie = new Cookie(“user”, “Tom”); response.addCookie(cookie);
这样,客户端浏览器就会收到一个包含user=Tom的Set-Cookie头,并将其保存在本地。
Cookie cookie = new Cookie("ticket", map.get("ticket").toString());
//设置可以携带cookie的路径
cookie.setPath(contextPath);
//设置cookie的生存时间
cookie.setMaxAge(expiredSeconds);
//将cookie添加到HTTP响应中,从而发送给客户端浏览器
response.addCookie(cookie);
3、登陆拦截器
拦截器示例:
1、定义拦截器,实现HandlerInterceptor,根据自己的需求重写prehandle等方法
2、配置拦截器,为他指定拦截、排除的路径
拦截器应用:
1、在请求开始时查询登录用户
2、在本次请求中持有用户数据
3、在模板视图上显示用户数据
4、在请求结束时清理用户数据
示例如下代码所示
package com.nowcoder.community.controller.interceptor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
public class AlphaInterceptor implements HandlerInterceptor {
private static final Logger logger = LoggerFactory.getLogger(AlphaInterceptor.class);
// 在Controller之前执行
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
logger.debug("preHandle: " + handler.toString());
return true;
}
// 在Controller之后执行
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
logger.debug("postHandle: " + handler.toString());
}
// 在TemplateEngine之后执行
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
logger.debug("afterCompletion: " + handler.toString());
}
}
需要配置webconfig使得拦截器生效
package com.nowcoder.community.config;
import com.nowcoder.community.controller.interceptor.AlphaInterceptor;
import com.nowcoder.community.controller.interceptor.LoginRequiredInterceptor;
import com.nowcoder.community.controller.interceptor.LoginTicketInterceptor;
import com.nowcoder.community.controller.interceptor.MessageInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private AlphaInterceptor alphaInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(alphaInterceptor)
//excludePathPatterns排除以下路径,在以下路径拦截器并不会作用
.excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg") //以下路径拦截器生效
.addPathPatterns("/register", "/login");
}
}
输入请求路径login,看看日志输出,检查拦截器是否发生作用
打印出handler,说明login触发了LoginController类内部的方法,从而拦截器效果
具体拦截器的操作见上一篇文章
3、用户信息的存储
ThreadLocal<User>users是一个用于存储每个线程独有的User对象的类,它可以保证每个线程都可以访问自己的User对象,而不会受到其他线程的影响ThreadLocal类提供了get()和set()方法来获取和设置当前线程的User对象。ThreadLocal类还可以通过重写initialValue()方法来指定User对象的初始值1 。
在这个项目中,采用ThreadLocal来存储用户的信息,确保每个用户线程访问到的都是自己的User对象,不受到其他用户线程的影响
/**
* 持有用户信息,用于代替session对象.
*/
@Component
public class HostHolder {
private ThreadLocal<User> users = new ThreadLocal<>();
public void setUser(User user) {
users.set(user);
}
public User getUser() {
return users.get();
}
public void clear() {
users.remove();
}
}
我们可以看看Threadlocal.set和get方法的源码,可以看出对Threadlocal变量操作,都是对于当前线程的变量进行操作,具有隔离性,其他线程无法操作当前线程内Threadlocal的值
/**
* Sets the current thread's copy of this thread-local variable
* to the specified value. Most subclasses will have no need to
* override this method, relying solely on the {@link #initialValue}
* method to set the values of thread-locals.
*
* @param value the value to be stored in the current thread's copy of
* this thread-local.
*/
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
/**
* Returns the value in the current thread's copy of this
* thread-local variable. If the variable has no value for the
* current thread, it is first initialized to the value returned
* by an invocation of the {@link #initialValue} method.
*
* @return the current thread's value of this thread-local
*/
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
4、fastjson的使用
即将java字符串,对象等转换为json对象,或将json对象转换为java字符串对象等,输出结果如下图所示
//转换为json字符串
public static String getJSONString(int code, String msg, Map<String, Object> map) {
JSONObject json = new JSONObject();
json.put("code", code);
json.put("msg", msg);
if (map != null) {
for (String key : map.keySet()) {
json.put(key, map.get(key));
}
}
return json.toJSONString();
}
public static void main(String[] args) {
Map<String, Object> map = new HashMap<>();
map.put("name", "zhangsan");
map.put("age", 25);
System.out.println(getJSONString(0, "ok", map));
}
5、spring中的事务操作
@Transaction注解解释可以看上一篇牛客论坛内容。
什么场景下需要事务回滚?
当涉及到多个数据表或多个数据源的操作的时候,就需要使用事务来保证事务操作的原子性。例如,再电商系统中,用户下单需要同时更新用户表、订单表、库存表等。如果一个操作失败,需要回滚所有操作,避免用户扣款但是订单未生成 或者库存未减少等问题。
或者如批量事务操作的时候,一个,两个事务分别要拿对方持有的行锁,会产生死锁,对于mysql来说死锁就是CPU飙升,最后两个事务会发生回滚。
- @Transaction注解是一种声明式事务管理的方式,它可以在类或者方法上使用,表示该类或者方法需要进行事务管理
- @Transaction注解在什么时候进行回滚操作,取决于它的属性和异常情况。默认情况下,@Transaction注解只会在抛出运行时异常(RuntimeException)或者错误(Error)时才会回滚事务
- @Transaction注解如何进行回滚操作,有以下几种方式:
事务回滚的代码示例(采用注解方式全局回滚)
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserDAO userDAO;
@Autowired
private OrderDAO orderDAO;
//使用@Transactional注解开启事务管理
@Transactional
public void updateUserAndOrder(User user, Order order) {
try {
//更新用户表
userDAO.updateUser(user);
//更新订单表
orderDAO.updateOrder(order);
} catch (Exception e) {
//如果发生异常,抛出运行时异常,让spring事务管理器捕获并回滚事务
throw new RuntimeException(e);
}
}
}
采TransactionTemplate进行方法体内的局部代码回滚,代码示例
@Service
public class UserService {
@Autowired
private UserDAO userDAO;
@Autowired
private OrderDAO orderDAO;
//注入TransactionTemplate对象
@Autowired
private TransactionTemplate transactionTemplate;
//使用transactionTemplate.execute方法开启事务管理
public void updateUserAndOrder(User user, Order order) {
transactionTemplate.execute(new TransactionCallback<Object>() {
//在doInTransaction方法中编写事务操作的逻辑
public Object doInTransaction(TransactionStatus status) {
try {
//更新用户表
userDAO.updateUser(user);
//更新订单表
orderDAO.updateOrder(order);
} catch (Exception e) {
//如果发生异常,设置回滚标志
status.setRollbackOnly();
}
return null;
}
});
}
}
6、Redis优化登陆模块
6.1、验证码存储位置转移
原本将验证码存放于session中,验证时需要拿用户的验证码与session的验证码做对比,在这里采用redis存储验证码,进行了优化。
使用redis存储验证码(验证码需要频繁刷新和访问,对性能要求较高,并且验证码不需要永久保存,通常在很短的时间内就会失效,而且分布式部署时,存在session问题)
具体操作:为了分辨这是哪一个用户登录所需要的验证码,定义一个随机字符串key,分别存储于浏览器的cookie中和redis中,redis以随机字符串为键,以验证码为值,去判断与用户输入的验证码是否相同。
代码:生成验证码和随机字符串,随机字符串存入cookie中,随机字符串还与验证码作为键值对存入redis中
@RequestMapping(path = "/kaptcha", method = RequestMethod.GET)
public void getKaptcha(HttpServletResponse response/*, HttpSession session*/) {
// 生成验证码
String text = kaptchaProducer.createText();
BufferedImage image = kaptchaProducer.createImage(text);
// 将验证码存入session
// session.setAttribute("kaptcha", text);
// 验证码的归属
String kaptchaOwner = CommunityUtil.generateUUID();
//设置过期时间
Cookie cookie = new Cookie("kaptchaOwner", kaptchaOwner);
cookie.setMaxAge(60);
cookie.setPath(contextPath);
response.addCookie(cookie);
// 将验证码存入Redis
String redisKey = RedisKeyUtil.getKaptchaKey(kaptchaOwner);
redisTemplate.opsForValue().set(redisKey, text, 60, TimeUnit.SECONDS);
// 将突图片输出给浏览器
response.setContentType("image/png");
try {
OutputStream os = response.getOutputStream();
ImageIO.write(image, "png", os);
} catch (IOException e) {
logger.error("响应验证码失败:" + e.getMessage());
}
}
登录时,通过浏览器的cookie取得该用户的特有的标识符,通过标识符找到redis中对应的值,与用户输入的验证码进行比对,并校验账号密码是否正确。
@RequestMapping(path = "/login", method = RequestMethod.POST)
public String login(String username, String password, String code, boolean rememberme,
Model model, /*HttpSession session, */HttpServletResponse response,
@CookieValue("kaptchaOwner") String kaptchaOwner) {
// 检查验证码
// String kaptcha = (String) session.getAttribute("kaptcha");
String kaptcha = null;
if (StringUtils.isNotBlank(kaptchaOwner)) {
String redisKey = RedisKeyUtil.getKaptchaKey(kaptchaOwner);
kaptcha = (String) redisTemplate.opsForValue().get(redisKey);
}
if (StringUtils.isBlank(kaptcha) || StringUtils.isBlank(code) || !kaptcha.equalsIgnoreCase(code)) {
model.addAttribute("codeMsg", "验证码不正确!");
return "/site/login";
}
// 检查账号,密码
int expiredSeconds = rememberme ? REMEMBER_EXPIRED_SECONDS : DEFAULT_EXPIRED_SECONDS;
Map<String, Object> map = userService.login(username, password, expiredSeconds);
if (map.containsKey("ticket")) {
Cookie cookie = new Cookie("ticket", map.get("ticket").toString());
cookie.setPath(contextPath);
cookie.setMaxAge(expiredSeconds);
response.addCookie(cookie);
return "redirect:/index";
} else {
model.addAttribute("usernameMsg", map.get("usernameMsg"));
model.addAttribute("passwordMsg", map.get("passwordMsg"));
return "/site/login";
}
}
6.2 登录凭证存储位置转移
将用户生成的登录凭证存储到redis中
登录的部分示例代码,登录时用户生成一个登录凭证,存进redis中,也存进cookie中(不安全),设置状态为0,即在线状态
退出登录:即将登录状态改为1,为失效状态
代码如下:
public void logout(String ticket) {
// loginTicketMapper.updateStatus(ticket, 1);
String redisKey = RedisKeyUtil.getTicketKey(ticket);
LoginTicket loginTicket = (LoginTicket) redisTemplate.opsForValue().get(redisKey);
loginTicket.setStatus(1);
redisTemplate.opsForValue().set(redisKey, loginTicket);
}
检查登录状态:即拿着cookie中的ticket去redis中找loginticket(其实是很不安全的)
代码如下:
public LoginTicket findLoginTicket(String ticket) {
// return loginTicketMapper.selectByTicket(ticket);
String redisKey = RedisKeyUtil.getTicketKey(ticket);
return (LoginTicket) redisTemplate.opsForValue().get(redisKey);
}
其实这里可以采用JWT token的方式实现单点登录,可避免遭受攻击
6.2.1、什么是JWT
Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).
该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。
JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。
即一个token字符串,里面可以保存用户的id等非秘密信息,通过jwt加密,服务端的密钥生成token;相当于把用户信息压缩;等到了服务端再进行解码;
可以来一段代码示例,可以清楚弄懂机制
将用户信息用jwt及服务端密钥生成token,并将token存入服务端(redis中),用于验证用户的都登录状态
配置登录拦截器,未登录不可访问的路径:
package com.mzlu.blog.config;
import com.mzlu.blog.handler.LoginInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;
@Override
public void addCorsMappings(CorsRegistry registry) {
//跨域配置,不可设置为*,不安全, 前后端分离项目,可能域名不一致
//本地测试 端口不一致 也算跨域
registry.addMapping("/**").allowedOrigins("http://localhost:8080");
}
/**
* 拦截器
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
//拦截test接口,后续实际遇到需要拦截的接口时,在配置为真正的拦截接口
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/test")
.addPathPatterns("/comments/create/change")
.addPathPatterns("/articles/publish");
}
}
拦截器prehandle方法所做的事情:
1、从请求头的Authorization中获取到用户的token,如果请求头中没有token,说明用户根本没有登录;
2、如果说请求头中有token,首先根据服务端的密钥进解密,看这个token是否合法(可能存在伪造情况),
3、再会去redis中看看登陆状态是否过期;如果没有过期,则将其token解密,将用户信息解析出来,放到threadlocal中。
package com.mzlu.blog.handler;
import com.alibaba.fastjson.JSON;
import com.mysql.cj.util.StringUtils;
import com.mzlu.blog.dao.pojo.SysUsers;
import com.mzlu.blog.service.LoginService;
import com.mzlu.blog.utils.UserThreadLocal;
import com.mzlu.blog.vo.ErrorCode;
import com.mzlu.blog.vo.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
@Autowired
private LoginService loginService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (!(handler instanceof HandlerMethod)){
return true;
}
String token = request.getHeader("Authorization");
log.info("=================request start===========================");
String requestURI = request.getRequestURI();
log.info("request uri:{}",requestURI);
log.info("request method:{}",request.getMethod());
log.info("token:{}", token);
log.info("=================request end===========================");
if (token == null){
Result result = Result.fail(ErrorCode.NO_LOGIN.getCode(), "未登录");
response.setContentType("application/json;charset=utf-8");
response.getWriter().print(JSON.toJSONString(result));
return false;
}
SysUsers sysUser = loginService.checkToken(token);
if (sysUser == null){
Result result = Result.fail(ErrorCode.NO_LOGIN.getCode(), "未登录");
response.setContentType("application/json;charset=utf-8");
response.getWriter().print(JSON.toJSONString(result));
return false;
}
//是登录状态,放行
//希望在controller中直接获取用户的信息,怎么获取?
System.out.println("-------------------------------");
System.out.println("yonghuxinxi"+ sysUser);
System.out.println("-------------------------------");
UserThreadLocal.put(sysUser);
return true;
}
/**
* 删除ThreadLocal中的值
* @param request
* @param response
* @param handler
* @param ex
* @throws Exception
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//如果不删除ThreadLocal中的值,便有内存泄露的风险
UserThreadLocal.remove();
}
}
JWT工具类:生成token,及根据密钥检验token的合法性
package com.mzlu.blog.utils;
import io.jsonwebtoken.Jwt;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* JWT工具类
*/
public class JWTUtils {
private static final String jwtToken = "123456Mszlu!@#$$";
public static String createToken(Long userId){
Map<String,Object> claims = new HashMap<>();
claims.put("userId",userId);
JwtBuilder jwtBuilder = Jwts.builder()
.signWith(SignatureAlgorithm.HS256, jwtToken) // 签发算法,秘钥为jwtToken
.setClaims(claims) // body数据,要唯一,自行设置
.setIssuedAt(new Date()) // 设置签发时间
.setExpiration(new Date(System.currentTimeMillis() + 24 * 60 * 60 * 60 * 1000));// 一天的有效时间
String token = jwtBuilder.compact();
return token;
}
//用户验证,看看是否合法
public static Map<String, Object> checkToken(String token){
try {
Jwt parse = Jwts.parser().setSigningKey(jwtToken).parse(token);
return (Map<String, Object>) parse.getBody();
}catch (Exception e){
e.printStackTrace();
}
return null;
}
public static void main(String[] args) {
String token=JWTUtils.createToken(100L);
System.out.println(token);
Map<String,Object> map=JWTUtils.checkToken(token);
System.out.println(map.get("userId"));
}
}
用户登录,生成token,将token存放进request域的“Authorzation”中和服务端的redis中
package com.mzlu.blog.service.impl;
import com.alibaba.fastjson.JSON;
import com.mysql.cj.util.StringUtils;
import com.mzlu.blog.dao.pojo.SysUsers;
import com.mzlu.blog.service.LoginService;
import com.mzlu.blog.service.SysUserService;
import com.mzlu.blog.utils.JWTUtils;
import com.mzlu.blog.vo.ErrorCode;
import com.mzlu.blog.vo.Result;
import com.mzlu.blog.vo.params.LoginParam;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@Service
public class LoginImpl implements LoginService {
@Autowired
private SysUserService sysUserService;
@Autowired
private RedisTemplate<String,String> redisTemplate;
//设置加密盐
private static final String slat = "mszlu!@#";
@Override
public Result login(LoginParam loginParam) {
/**
* 1.检查参数是否合法
* 2.根据用户名和密码去user表中查询是否存在
* 3.如果不存在 登录失败
* 4.如果存在,使用jwt 生成token 返回给前端
* 5.把token放入redis中,redis token:user信息 设置过期时间(登录认证时,先验证字符串是否合法,再判断是否存在)
*/
String account=loginParam.getAccount();
String password=loginParam.getPassword();
//判断用户名和密码是否为空
if(StringUtils.isEmptyOrWhitespaceOnly(account)||StringUtils.isEmptyOrWhitespaceOnly(password)){
return Result.fail(ErrorCode.PARAMS_ERROR.getCode(),ErrorCode.PARAMS_ERROR.getMsg());
}
password= DigestUtils.md5Hex(password + slat);
//判断用户名和密码是否储存在数据库中
SysUsers sysUsers=sysUserService.findUser(account,password);
if(sysUsers==null){
return Result.fail(ErrorCode.ACCOUNT_PWD_NOT_EXIST.getCode(),ErrorCode.ACCOUNT_PWD_NOT_EXIST.getMsg());
}
System.out.println("------------------------");
System.out.print("sysuser"+sysUsers.getId());
String token= JWTUtils.createToken(sysUsers.getId());
//用redisTemplate向redis中存放键值,过期时间
redisTemplate.opsForValue().set("TOKEN_"+token, JSON.toJSONString(sysUsers),1, TimeUnit.DAYS);
return Result.success(token);
}
@Override
public SysUsers checkToken(String token) {
//先检验token是否为空
if(StringUtils.isEmptyOrWhitespaceOnly(token)){
return null;
}
//再检验JWT用户中是否保存
Map<String,Object>stringObjectMap=JWTUtils.checkToken(token);
if(stringObjectMap==null){
return null;
}
//从redis拿出来,看看键值是否为空
String userJson=redisTemplate.opsForValue().get("TOKEN_"+token);
System.out.println("这是json"+userJson);
if(StringUtils.isEmptyOrWhitespaceOnly(userJson)) {
return null;
}
//用fastJson将字符串转化为类对象
SysUsers sysUsers=JSON.parseObject(userJson,SysUsers.class);
return sysUsers;
}
6.2.2、JWT的组成
1、JWT生成编码后的样子
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.UQmqAUhUrpDVV2ST7mZKyLTomVfg7sYkEjmdDI5XF8Q
复制代码
2、JWT由三部分构成
第一部分我们称它为头部(header),第二部分我们称其为载荷(payload, 类似于飞机上承载的物品),第三部分是签证(signature).
header
jwt的头部承载两部分信息:
-
声明类型,这里是jwt
-
声明加密的算法 通常直接使用 HMAC SHA256
完整的头部就像下面这样的JSON:
{ 'typ': 'JWT', 'alg': 'HS256'}
复制代码
然后将头部进行base64加密(该加密是可以对称解密的),构成了第一部分
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
复制代码
playload
载荷就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分
-
标准中注册的声明
-
公共的声明
-
私有的声明
标准中注册的声明 (建议但不强制使用) :
-
iss: jwt签发者
-
sub: jwt所面向的用户
-
aud: 接收jwt的一方
-
exp: jwt的过期时间,这个过期时间必须要大于签发时间
-
nbf: 定义在什么时间之前,该jwt都是不可用的.
-
iat: jwt的签发时间
-
jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
公共的声明 :
公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.
私有的声明 :
私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。
定义一个payload:
{ "sub": "1234567890", "name": "John Doe", "admin": true}
复制代码
然后将其进行base64加密,得到Jwt的第二部分
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
复制代码
signature
jwt的第三部分是一个签证信息,这个签证信息由三部分组成:
-
header (base64后的)
-
payload (base64后的)
-
secret
这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串(头部在前),然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。
UQmqAUhUrpDVV2ST7mZKyLTomVfg7sYkEjmdDI5XF8Q
复制代码
密钥secret是保存在服务端的,服务端会根据这个密钥进行生成token和验证,所以需要保护好。
3、签名的目的
最后一步签名的过程,实际上是对头部以及载荷内容进行签名。一般而言,加密算法对于不同的输入产生的输出总是不一样的。对于两个不同的输入,产生同样的输出的概率极其地小(有可能比我成世界首富的概率还小)。所以,我们就把“不一样的输入产生不一样的输出”当做必然事件来看待吧。
所以,如果有人对头部以及载荷的内容解码之后进行修改,再进行编码的话,那么新的头部和载荷的签名和之前的签名就将是不一样的。而且,如果不知道服务器加密的时候用的密钥的话,得出来的签名也一定会是不一样的。
服务器应用在接受到JWT后,会首先对头部和载荷的内容用同一算法再次签名。那么服务器应用是怎么知道我们用的是哪一种算法呢?别忘了,我们在JWT的头部中已经用alg字段指明了我们的加密算法了。
如果服务器应用对头部和载荷再次以同样方法签名之后发现,自己计算出来的签名和接受到的签名不一样,那么就说明这个Token的内容被别人动过的,我们应该拒绝这个Token,返回一个HTTP 401 Unauthorized响应。
注意:在JWT中,不应该在载荷里面加入任何敏感的数据,比如用户的密码。
4、如何应用
一般是在请求头里加入Authorization,并加上Bearer标注:
fetch('api/user/1', { headers: { 'Authorization': 'Bearer ' + token }})
复制代码
服务端会验证token,如果验证通过就会返回相应的资源。
5、安全相关
-
不应该在jwt的payload部分存放敏感信息,因为该部分是客户端可解密的部分。
-
保护好secret私钥,该私钥非常重要。
-
如果可以,请使用https协议
6、对Token认证的五点认识
-
一个Token就是一些信息的集合;
-
在Token中包含足够多的信息,以便在后续请求中减少查询数据库的几率;
-
服务端需要对cookie和HTTP Authrorization Header进行Token信息的检查;
-
基于上一点,你可以用一套token认证代码来面对浏览器类客户端和非浏览器类客户端;
-
因为token是被签名的,所以我们可以认为一个可以解码认证通过的token是由我们系统发放的,其中带的信息是合法有效的;
三、传统的session认证
我们知道,http协议本身是一种无状态的协议,而这就意味着如果用户向我们的应用提供了用户名和密码来进行用户认证,那么下一次请求时,用户还要再一次进行用户认证才行,因为根据http协议,我们并不能知道是哪个用户发出的请求,所以为了让我们的应用能识别是哪个用户发出的请求,我们只能在服务器存储一份用户登录的信息,这份登录信息会在响应时传递给浏览器,告诉其保存为cookie,以便下次请求时发送给我们的应用,这样我们的应用就能识别请求来自哪个用户了,这就是传统的基于session认证。
但是这种基于session的认证使应用本身很难得到扩展,随着不同客户端用户的增加,独立的服务器已无法承载更多的用户,而这时候基于session认证应用的问题就会暴露出来。
基于session认证所显露的问题
Session: 每个用户经过我们的应用认证之后,我们的应用都要在服务端做一次记录,以方便用户下次请求的鉴别,通常而言session都是保存在内存中,而随着认证用户的增多,服务端的开销会明显增大。
扩展性: 用户认证之后,服务端做认证记录,如果认证的记录被保存在内存中的话,这意味着用户下次请求还必须要请求在这台服务器上,这样才能拿到授权的资源,这样在分布式的应用上,相应的限制了负载均衡器的能力。这也意味着限制了应用的扩展能力。
CSRF: 因为是基于cookie来进行用户识别的, cookie如果被截获,用户就会很容易受到跨站请求伪造的攻击。
基于token的鉴权机制
基于token的鉴权机制类似于http协议也是无状态的,它不需要在服务端去保留用户的认证信息或者会话信息。这就意味着基于token认证机制的应用不需要去考虑用户在哪一台服务器登录了,这就为应用的扩展提供了便利。
流程上是这样的:
-
用户使用用户名密码来请求服务器
-
服务器进行验证用户的信息
-
服务器通过验证发送给用户一个token
-
客户端存储token,并在每次请求时附送上这个token值
-
服务端验证token值,并返回数据
这个token必须要在每次请求时传递给服务端,它应该保存在请求头里, 另外,服务端要支持CORS(跨来源资源共享)策略,一般我们在服务端这么做就可以了 Access-Control-Allow-Origin:*。
四、token的优点
-
支持跨域访问: Cookie是不允许垮域访问的,这一点对Token机制是不存在的,前提是传输的用户认证信息通过HTTP头传输。
-
无状态(也称:服务端可扩展行):Token机制在服务端不需要存储session信息,因为Token 自身包含了所有登录用户的信息,只需要在客户端的cookie或本地介质存储状态信息。
-
更适用CDN: 可以通过内容分发网络请求你服务端的所有资料(如:javascript,HTML,图片等),而你的服务端只要提供API即可。
-
去耦: 不需要绑定到一个特定的身份验证方案。Token可以在任何地方生成,只要在你的API被调用的时候,你可以进行Token生成调用即可。
-
更适用于移动应用: 当你的客户端是一个原生平台(iOS, Android,Windows 8等)时,Cookie是不被支持的(你需要通过Cookie容器进行处理),这时采用Token认证机制就会简单得多。
-
CSRF:因为不再依赖于Cookie,所以你就不需要考虑对CSRF(跨站请求伪造)的防范。
-
性能: 一次网络往返时间(通过数据库查询session信息)总比做一次HMACSHA256计算 的Token验证和解析要费时得多。
-
不需要为登录页面做特殊处理: 如果你使用Protractor 做功能测试的时候,不再需要为登录页面做特殊处理。
-
基于标准化:你的API可以采用标准化的 JSON Web Token (JWT). 这个标准已经存在多个后端库(.NET, Ruby, Java,Python, PHP)和多家公司的支持(如:Firebase,Google, Microsoft)。
-
因为json的通用性,所以JWT是可以进行跨语言支持的,像JAVA,JavaScript,NodeJS,PHP等很多语言都可以使用。
-
因为有了payload部分,所以JWT可以在自身存储一些其他业务逻辑所必要的非敏感信息,到服务端再进行解密,减少了空间。
-
便于传输,jwt的构成非常简单,字节占用很小,所以它是非常便于传输的。
-
它不需要在服务端保存会话信息, 所以它易于应用的扩展。
jwt仍然需要注意的地方:
将token放在请求头中的Authorization中并不一定安全,因为token可能会被截获或者泄露1。为了提高安全性,您可以采取以下一些措施:
- 使用https协议来加密传输token,防止被中间人攻击
- 使用较短的token过期时间,减少token被盗用的风险
- 使用一些额外的信息来增强token的安全性,比如用户的IP地址、设备信息等
- 使用一些开源的库或者框架来实现token的生成和验证,比如spring security oauth2。
6.3 使用redis作为缓存,缓存用户信息
将用户信息也存到redis中,作为缓存;如果有更改,则更新缓存。
7、kafka消息队列的使用
消息队列的作用:
1、通过异步处理提高系统性能(减少响应所需时间)
2、削峰/限流
3、降低系统耦合性
作为系统的消息通知:比如一个用户给另一个用户点赞,则另一个用户会收到系统发布点赞的消息;
对于点赞、评论、关注三种不同的事件,可以定义三种不同的topic;
消息队列是可以异步的,生产者生产出消息后,放进消息队列后,不用管后面的事情,由消费者线程去解决它。
我们可以来看一下代码示例:定义消费者(consumer)和生产者(producer)
生产者定义主题,即定义指定的消息队列,往队列内发送消息
消费者定义需要监听的主题,即分配好需要处理的消息通道有哪些,进行处理
package com.nowcoder.community;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Component;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest
@ContextConfiguration(classes = CommunityApplication.class)
public class KafkaTests {
@Autowired
private KafkaProducer kafkaProducer;
@Test
public void testKafka() {
//生产者发送消息,定义主题为“test”
kafkaProducer.sendMessage("test", "你好");
kafkaProducer.sendMessage("test", "在吗");
try {
Thread.sleep(1000 * 10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
@Component
class KafkaProducer {
@Autowired
private KafkaTemplate kafkaTemplate;
public void sendMessage(String topic, String content) {
kafkaTemplate.send(topic, content);
}
}
@Component
class KafkaConsumer {
//消费者处理监听到指定主题的消息,进行消费
@KafkaListener(topics = {"test"})
public void handleMessage(ConsumerRecord record) {
System.out.println(record.value());
}
}
在此项目中,因为消息通知的内容是要写进数据库的;因此,比如当用户给某人点赞,点赞事件触发了消息队列的使用:A给B点赞之后,生产者生产出点赞的消息内容(如谁给谁点赞,是对用户点赞还是用户的文章,还是对用户的评论点赞),消费者拿到后去处理(将这些内容写入数据库),
即点赞和点赞的内容写入数据库这两部分是异步的,点赞事件无需等待数据写入数据库后才算完成。
代码如下所示:
消费者:
package com.nowcoder.community.event;
import com.alibaba.fastjson.JSONObject;
import com.nowcoder.community.entity.DiscussPost;
import com.nowcoder.community.entity.Event;
import com.nowcoder.community.entity.Message;
import com.nowcoder.community.service.DiscussPostService;
import com.nowcoder.community.service.ElasticsearchService;
import com.nowcoder.community.service.MessageService;
import com.nowcoder.community.util.CommunityConstant;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@Component
public class EventConsumer implements CommunityConstant {
private static final Logger logger = LoggerFactory.getLogger(EventConsumer.class);
@Autowired
private MessageService messageService;
@Autowired
private DiscussPostService discussPostService;
@Autowired
private ElasticsearchService elasticsearchService;
@KafkaListener(topics = {TOPIC_COMMENT, TOPIC_LIKE, TOPIC_FOLLOW})
public void handleCommentMessage(ConsumerRecord record) {
if (record == null || record.value() == null) {
logger.error("消息的内容为空!");
return;
}
Event event = JSONObject.parseObject(record.value().toString(), Event.class);
if (event == null) {
logger.error("消息格式错误!");
return;
}
// 发送站内通知
Message message = new Message();
message.setFromId(SYSTEM_USER_ID);
message.setToId(event.getEntityUserId());
message.setConversationId(event.getTopic());
message.setCreateTime(new Date());
Map<String, Object> content = new HashMap<>();
content.put("userId", event.getUserId());
content.put("entityType", event.getEntityType());
content.put("entityId", event.getEntityId());
if (!event.getData().isEmpty()) {
for (Map.Entry<String, Object> entry : event.getData().entrySet()) {
content.put(entry.getKey(), entry.getValue());
}
}
message.setContent(JSONObject.toJSONString(content));
messageService.addMessage(message);
}
// 消费发帖事件
@KafkaListener(topics = {TOPIC_PUBLISH})
public void handlePublishMessage(ConsumerRecord record) {
if (record == null || record.value() == null) {
logger.error("消息的内容为空!");
return;
}
Event event = JSONObject.parseObject(record.value().toString(), Event.class);
if (event == null) {
logger.error("消息格式错误!");
return;
}
DiscussPost post = discussPostService.findDiscussPostById(event.getEntityId());
elasticsearchService.saveDiscussPost(post);
}
}
生产者代码如下所示:
package com.nowcoder.community.event;
import com.alibaba.fastjson.JSONObject;
import com.nowcoder.community.entity.Event;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Component;
@Component
public class EventProducer {
@Autowired
private KafkaTemplate kafkaTemplate;
// 处理事件
public void fireEvent(Event event) {
// 将事件发布到指定的主题
kafkaTemplate.send(event.getTopic(), JSONObject.toJSONString(event));
}
}
8、通过建立缓存优化网站的性能
因为对MYSQL的操作是读取磁盘,速度并不快,可以使用redis(基于内存读取),caffeine(高性能本地缓存库)作为缓存,读取mysql的数据,用户可以从缓存中读取干净的数据,提高响应速度,对于修改过的数据,缓存会进行同步。
这个项目中采用多级缓存,缓存文章列表和十大热门文章,采用caffeine作为本地缓存(针对单个用户的缓存),采用redis作为分布式缓存(存储所有用户的缓存),用户读取数据时,优先读取本地缓存,如果本地缓存没有,则去redis的缓存中读取,如果还没有,才去DB读取;同步顺序倒过来,即DB先同步redis中的缓存,再去同步用户的本地缓存:
即所有用户是共享分布式缓存的,独享本地缓存(caffeine)
8.1 有哪些常见的缓存
8.2 caffeine与redis相比,相同和不同之处:
caffeine是存在服务器的缓存,它是一个基于Java的本地缓存库,跟redis相比有以下相同和不同的地方:
- 相同点:都是用来存储键值对数据的缓存,都支持过期策略和回收策略,都可以提高数据访问的效率。
- 不同点:caffeine是一个内存缓存,redis是一个分布式缓存;caffeine只能在单个进程内使用,redis可以在多个进程或者多个服务器之间共享数据;caffeine使用Window TinyLfu回收策略,redis使用LRU或者LFU回收策略;caffeine不需要网络通信,redis需要网络通信。
8.3 多级缓存的优势在于
多级缓存相比单级缓存的优势在于:
- 可以利用不同层级的缓存特性,比如应用层缓存可以提供最快的响应速度(如caffeine),分布式缓存可以提供高可用性和一致性,系统层缓存(操作系统层面)可以提供更大的容量和更低的成本等。
- 可以降低数据库压力,减少网络开销,提高系统性能。
- 可以增强系统的可用性和容错性,当某一层缓存出现故障或者不可用时,可以从其他层缓存或者数据库中获取数据。
8.4 基于注解的caffeine简单使用
1、引入maven配置
// 引入caffeine和spring cache的依赖
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
2、在配置文件中加入配置
// 在配置文件中设置缓存的名称和参数
spring:
cache:
cache-names: user, product # 缓存名称
caffeine:
spec: maximumSize=1000,expireAfterWrite=10m # 公共参数
user: maximumSize=500,expireAfterAccess=5m # 用户缓存参数
3、在项目启动类上添加@EnableCaching注解,开启缓存功能
// 在启动类上添加@EnableCaching注解,开启缓存功能
@SpringBootApplication
@EnableCaching // 开启缓存功能
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
4、定义一个用户实体类
// 定义一个用户实体类,使用lombok简化代码
@Data // 自动生成get/set/toString等方法
@AllArgsConstructor // 自动生成全参构造方法
@NoArgsConstructor // 自动生成无参构造方法
public class User {
private String id;
private String name;
private int age;
}
5、定义一个用户服务接口类,模拟数据库操作和缓存操作
// 定义一个用户服务接口类,模拟数据库操作和缓存操作
public interface UserService {
// 获取用户,如果缓存中没有则从数据库或其他地方获取,并放入缓存
@Cacheable(value = "user", key = "#id")
User getUser(String id);
// 更新用户,同时更新数据库或其他地方的数据,并更新缓存
@CachePut(value = "user", key = "#user.id")
User updateUser(User user);
// 删除用户,同时删除数据库或其他地方的数据,并删除缓存
@CacheEvict(value = "user", key = "#id")
void deleteUser(String id);
}
6、定义一个用户实现类,实现接口方法,并模拟数据库操作和缓存操作
// 定义一个用户服务实现类,实现接口方法,并模拟数据库操作和缓存操作
@Service
public class UserServiceImpl implements UserService {
// 模拟一个数据库,用map存储用户数据
private Map<String, User> userMap = new HashMap<>();
// 初始化一些用户数据
public UserServiceImpl() {
userMap.put("1", new User("1", "张三", 20));
userMap.put("2", new User("2", "李四", 21));
userMap.put("3", new User("3", "王五", 22));
}
@Override
public User getUser(String id) {
System.out.println("从数据库中获取用户:" + id);
return userMap.get(id);
}
@Override
public User updateUser(User user) {
System.out.println("更新数据库中的用户:" + user);
userMap.put(user.getId(), user);
return user;
}
@Override
public void deleteUser(String id) {
System.out.println("删除数据库中的用户:" + id);
userMap.remove(id);
}
}
7、定义一个测试控制器类,用来测试缓存效果
// 定义一个测试控制器类,用来测试缓存的效果
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
// 根据id获取用户信息,第一次访问会从数据库中获取,之后会从缓存中获取
@GetMapping("/{id}")
public User getUser(@PathVariable String id) {
return userService.getUser(id);
}
// 更新用户信息,会同时更新数据库和缓存
@PutMapping("/{id}")
public User updateUser(@PathVariable String id, @RequestParam String name, @RequestParam int age) {
User user = new User(id, name, age);
return userService.updateUser(user);
}
// 删除用户信息,会同时删除数据库和缓存
@DeleteMapping("/{id}")
public void deleteUser(@PathVariable String id) {
userService.deleteUser(id);
}
}