当前位置: 首页 > news >正文

牛客过第二遍

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 {@Autowiredprivate AlphaDao alphaDao;@Autowiredprivate UserMapper userMapper;@Autowiredprivate DiscussPostMapper discussPostMapper;@Autowiredprivate TransactionTemplate transactionTemplate;public AlphaService() {
//        System.out.println("实例化AlphaService");}@PostConstructpublic void init() {
//        System.out.println("初始化AlphaService");}@PreDestroypublic 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>() {@Overridepublic 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的用法如下:

cookie.setPath是一个方法,用于设置cookie的path属性。path属性指定了哪些URL的请求可以携带cookie。如果不设置path属性,那么默认值是创建cookie的应用的路径,这意味着只有同一个应用可以访问这个cookie。如果设置了path属性,那么所有以该路径为前缀的URL都可以访问这个cookie。例如,如果设置了cookie.setPath(“/test”),那么/test, /test/, /test/a, /test/b等URL都可以访问这个cookie,但是/, /doc, /fr/test等URL则不能访问这个cookie。

response.addCookie是一个方法,用于将cookie添加到HTTP响应中,从而发送给客户端浏览器。这个方法需要一个Cookie对象作为参数,Cookie对象可以用Cookie(name, value)构造器创建,其中name和value是字符串类型。

例如,如果要创建一个名为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之前执行@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {logger.debug("preHandle: " + handler.toString());return true;}// 在Controller之后执行@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {logger.debug("postHandle: " + handler.toString());}// 在TemplateEngine之后执行@Overridepublic 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 {@Autowiredprivate AlphaInterceptor alphaInterceptor;@Overridepublic 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);elsecreateMap(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注解如何进行回滚操作,有以下几种方式:
    • 在方法中抛出运行时异常或者错误,让spring事务管理器捕获并回滚事务。
    • 在方法中使用try-catch语句块捕获异常,并在catch中手动调用TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()方法来设置回滚标志
    • 在@Transaction注解中使用rollbackFor属性来指定需要回滚的异常类数组,当方法中抛出指定异常时,则进行事务回滚

事务回滚的代码示例(采用注解方式全局回滚)

@Service
public class UserServiceImpl implements UserService {@Autowiredprivate UserDAO userDAO;@Autowiredprivate OrderDAO orderDAO;//使用@Transactional注解开启事务管理@Transactionalpublic 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 {@Autowiredprivate UserDAO userDAO;@Autowiredprivate OrderDAO orderDAO;//注入TransactionTemplate对象@Autowiredprivate 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);// 将验证码存入RedisString 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;@Overridepublic void addCorsMappings(CorsRegistry registry) {//跨域配置,不可设置为*,不安全, 前后端分离项目,可能域名不一致//本地测试 端口不一致 也算跨域registry.addMapping("/**").allowedOrigins("http://localhost:8080");}/*** 拦截器*/@Overridepublic 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 {@Autowiredprivate LoginService loginService;@Overridepublic 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*/@Overridepublic 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 {@Autowiredprivate SysUserService sysUserService;@Autowiredprivate RedisTemplate<String,String> redisTemplate;//设置加密盐private static final String slat = "mszlu!@#";@Overridepublic 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);}@Overridepublic 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 {@Autowiredprivate KafkaProducer kafkaProducer;@Testpublic void testKafka() {
//生产者发送消息,定义主题为“test”kafkaProducer.sendMessage("test", "你好");kafkaProducer.sendMessage("test", "在吗");try {Thread.sleep(1000 * 10);} catch (InterruptedException e) {e.printStackTrace();}}}@Component
class KafkaProducer {@Autowiredprivate 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);@Autowiredprivate MessageService messageService;@Autowiredprivate DiscussPostService discussPostService;@Autowiredprivate 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 {@Autowiredprivate 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));}@Overridepublic User getUser(String id) {System.out.println("从数据库中获取用户:" + id);return userMap.get(id);}@Overridepublic User updateUser(User user) {System.out.println("更新数据库中的用户:" + user);userMap.put(user.getId(), user);return user;}@Overridepublic void deleteUser(String id) {System.out.println("删除数据库中的用户:" + id);userMap.remove(id);}
}

7、定义一个测试控制器类,用来测试缓存效果

// 定义一个测试控制器类,用来测试缓存的效果
@RestController
@RequestMapping("/user")
public class UserController {@Autowiredprivate 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);}
}

相关文章:

牛客过第二遍

1、spring事务管理 1.1 Spring事务管理 声明式事务&#xff1a; 1 通过XML配置&#xff0c;声明某方法的事务特征 2、通过注解&#xff0c;声明某方法的事务特征&#xff0c;注解Transactional 1.2 Transactional 注解参数讲解 隔离级别传播行为回滚规则是否只读事务超时…...

科普:java与JavaScript的区别

Java和JavaScript是两种非常流行的编程语言&#xff0c;它们都有自己独特的特点和用途。尽管它们的名称相似&#xff0c;但实际上它们之间存在很多差异。在本文中&#xff0c;我们将详细介绍Java和JavaScript之间的区别。 一、Java和JavaScript的历史 Java是由Sun Microsyste…...

【教程】Unity 与 Simence PLC 联动通讯

开发平台&#xff1a;Unity 2021 依赖DLL&#xff1a;S7.NET 编程语言&#xff1a;CSharp 6.0 以上   一、前言 Unity 涉及应用行业广泛。在工业方向有着一定方向的涉足与深入。除构建数据看板等内容&#xff0c;也会有模拟物理设备进行虚拟孪生的需求需要解决。而 SIMATIC&a…...

视频编解码(一)之virtio-gpu环境搭建

一、基础概念 VA-API Video Acceleration API 是一组开源应用API接口&#xff0c;赋能应用&#xff08;比如VLC播放器、GStreamer等&#xff09;使用hardware video acceleration&#xff08;一般是GPU提供硬件视频加速功能&#xff09;&#xff0c;VA-API主要由开源库libva和…...

JDBC概述三(批处理+事务操作+数据库连接池)

一&#xff08;批处理&#xff09; 1.1 批处理简介 批处理&#xff0c;简而言之就是一次性执行多条SQL语句&#xff0c;在一定程度上可以提升执行SQL语句的速率。批处理可以通过使用Java的Statement和PreparedStatement来完成&#xff0c;因为这两个语句提供了用于处理批处理…...

MappedByteBuffer 详解(图解+秒懂+史上最全)

背景&#xff1a; 在尼恩视频版本里&#xff0c;从架构师视角&#xff0c;尼恩为大家彻底介绍 rocketmq 高可用、高并发中间件的原理与实操。 给大家底层的解读清楚 rocketmq 架构设计、源码设计、工业级高可用实操&#xff0c;含好多复杂度非常高、又非常核心的概念&#xff…...

顶点程序经典案例——树木生长

树木生长Shader一、介绍 大家好&#xff0c;我是阿赵。这次来做一个树木生长的Shader。 顶点程序作为整个渲染管线里面和片段程序并列的两大可控过程之一&#xff0c;一直存在感都比较低。我们平时制作的效果&#xff0c;很多都是在片段程序里面实现的计算&#xff0c;顶点程序…...

在云计算环境下保护Java应用程序的有效措施

云计算&#xff08;Cloud&#xff09;技术是近年来计算机科学的一个重要突破。大多数组织已经通过将自己的应用程序移入云平台而获益。不过&#xff0c;如何保证应用程序在第三方服务器上的安全性&#xff0c;是一项艰巨的挑战。 在本文中&#xff0c;我们将重点讨论Java&…...

vscode-markdown-代码片段及快捷键设置

代码片段及快捷键设置 主要为了插入表格和图片标签节约一点输入时间 代码片段设置 ctrlshiftp 打开面板输入 configure user snippets选择markdowncopy如下设置放入{}中 "tb4*4": {"prefix": "tb4*4","body": ["| $1 | $2 | $…...

ModelNet40数据集

跑PointNet,modelnet40数据集时; 有些人直接用.off文件;——【CAD模型】普林斯顿形状Banchmark中的.off文件遵循以下标准&#xff1a; OFF文件全是以OFF关键字开始的ASCII文件。下一行说明顶点的数量、面片的数量、边的数量。 边的数量可以安全地省略。对模型不会有影响(可以为…...

【都2023年了,还在问网络安全怎么入门】

前言 【都2023年了&#xff0c;还在问网络安全怎么入门】所以这一期就出一一个怎么学习网络安全的学习路线和方法&#xff0c;觉得有用的话点赞收藏下 首先咱们聊聊&#xff0c;学习网络安全方向通常会有哪些问题 1、打基础时间太长 学基础花费很长时间&#xff0c;光语言都有…...

Apple Xcode 14.3 (14E222b) 正式版发布下载

Command Line Tools for Xcode 14, tvOS 16 & watchOS 9 Simulator Runtime 请访问原文链接&#xff1a;https://sysin.org/blog/apple-xcode-14/&#xff0c;查看最新版。原创作品&#xff0c;转载请保留出处。 作者主页&#xff1a;sysin.org Xcode 14 包含了在所有 Ap…...

【Linux】sar常用选项介绍

sar 使用 安装sysstat apt-get install sysstat -y #或 yum install sysstat -y选项 用法: sar [ 选项 ] [ <时间间隔> [ <次数> ] ] 选项&#xff1a; [ -A ] [ -B ] [ -b ] [ -C ] [ -D ] [ -d ] [ -F [ MOUNT ] ] [ -H ] [ -h ] [ -p ] [ -q ] [ -r [ ALL ] ]…...

PHP 单笔转账到支付宝账户,支付宝公钥证书实现版本

支付宝某些业务只能使用公钥证书方式来验签 如&#xff1a;即使转账 红包等 笔者就要实现这样一个功能&#xff0c;【单笔转账到支付宝账户】&#xff0c;采用支付宝公钥证书签名来实现。 话不多说&#xff0c;流程先走起 第一步&#xff1a;下载支付宝秘钥生成器 由于我们使…...

第十四届蓝桥杯大赛软件赛省赛 C/C++ 大学 A 组 E 题

颜色平衡树问题描述格式输入格式输出样例输入样例输出评测用例规模与约定解析参考程序问题描述 格式输入 输入的第一行包含一个整数 n &#xff0c;表示树的结点数。 接下来 n 行&#xff0c;每行包含两个整数 Ci , Fi&#xff0c;用一个空格分隔&#xff0c;表示第 i 个结点 …...

Python 小型项目大全 21~25

二十一、DNA 可视化 原文&#xff1a;http://inventwithpython.com/bigbookpython/project21.html 脱氧核糖核酸是一种微小的分子&#xff0c;存在于我们身体的每个细胞中&#xff0c;包含着我们身体如何生长的蓝图。它看起来像一对核苷酸分子的双螺旋结构&#xff1a;鸟嘌呤、…...

MinIO从信息泄漏到RCE

文章目录信息泄露漏洞利用漏洞分析漏洞修复RCE漏洞分析参考文章信息泄露 漏洞利用 如果MinIO以集群方式部署&#xff0c;存在信息泄露漏洞&#xff0c;攻击者可以通过HTTP请求获取目标进程的所有环境变量&#xff0c;包括MINIO_SECRET_KEY和MINIO_ROOT_PASSWORD. vulhub有环…...

202.Spark(九):SparkStreaming案例实操

目录 一、启动zookeeper,kafka基础环境 二、项目导好jar包,并且创建源数据,并在kafka中测试能否消费到数据...

GlusterFS(GFS)分布式文件系统

目录 一.文件系统简介 1.文件系统的组成 2.文件系统的作用 3.文件系统的挂载使用 二.GlusterFS概述 1.GlusterFS是什么&#xff1f; 2.GlusterFS的特点 3.GlusterFS术语介绍 3.1 Brick&#xff08;存储块&#xff09; 3.2 Volume&#xff08;逻辑卷&#xff09; 3.3…...

ChatGPT文本框再次升级,打造出新型操作系统

在ChatGPT到来之前&#xff0c;没有谁能够预见。但是&#xff0c;它最终还是来了&#xff0c;并引起了不小的轰动&#xff0c;甚至有可能颠覆整个行业。 从某种程度上说&#xff0c;ChatGPT可能是历史上增长最快的应用程序&#xff0c;仅在两个多月就拥有了1亿多活跃用户&…...

DPU02国产USB转UART控制芯片替代CP2102

目录DPU02简介DPU02芯片特性应用DPU02简介 DPU02是高度集成的USB转UART的桥接控制芯片&#xff0c;该芯片为RS-232设计更新为USB设计&#xff0c;并简化PCB组件空间提供了一个简单的解决方案。       DPU02包括了一个USB 2.0全速功能控制器、USB收发器、振荡器、EEPROM和带…...

Softing新版HART多路复用器软件支持西门子控制器

用于访问配置和诊断数据的HART多路复用器软件——Softing smartLink SW-HT&#xff0c;现在支持西门子的ET200远程IO和FDT/DTM接口。 smartLink SW-HT是一个基于Docker容器的软件应用。通过该软件&#xff0c;用户可以快速地访问以太网远程IO的HART设备&#xff0c;并且无需额外…...

〖Python网络爬虫实战⑫〗- XPATH语法介绍

订阅&#xff1a;新手可以订阅我的其他专栏。免费阶段订阅量1000python项目实战 Python编程基础教程系列&#xff08;零基础小白搬砖逆袭) 说明&#xff1a;本专栏持续更新中&#xff0c;目前专栏免费订阅&#xff0c;在转为付费专栏前订阅本专栏的&#xff0c;可以免费订阅付费…...

实例方法、类方法、静态方法、实例属性、类属性

背景&#xff1a;今天在复习类相关知识的时候&#xff0c;突然想到这几种类型的方法的区别和用法&#xff0c;感觉有点模棱两可&#xff0c;于是总结一下&#xff0c;加深记忆。 定义&#xff1a;想要区别和理解几种方法&#xff0c;首先要定义一个类&#xff0c;要在类中加深…...

数据结构---二叉树

专栏&#xff1a;数据结构 个人主页&#xff1a;HaiFan. 专栏简介&#xff1a;这里是HaiFan.的数据结构专栏&#xff0c;今天的内容是二叉树。 二叉树树的概念及结构二叉树概念及结构二叉树的概念二叉树的存储结构二叉树的顺序结构及实现大根堆和小根堆堆的实现及其各个接口堆的…...

CMake——从入门到百公里加速6.7s

目录 一、前言 二、HelloWorld 三、CMAKE 界面 3.1 gui正则表达式 3.2 GUI构建 四 关键字 4.1 add_library 4.2 add_subdirectory 4.3 add_executable 4.4 aux_source_directory 4.5 SET设置变量 4.6 INSTALL安装 4.7 ADD_LIBRARY 4.8 SET_TARGET_PROPERTIES 4.9…...

无公网IP,在外公网远程访问RabbitMQ服务「内网穿透」

文章目录前言1.安装erlang 语言2.安装rabbitMQ3. 内网穿透3.1 安装cpolar内网穿透(支持一键自动安装脚本)3.2 创建HTTP隧道4. 公网远程连接5.固定公网TCP地址5.1 保留一个固定的公网TCP端口地址5.2 配置固定公网TCP端口地址前言 RabbitMQ是一个在 AMQP(高级消息队列协议)基础上…...

Node【二】NPM

文章目录&#x1f31f;前言&#x1f31f;NPM使用&#x1f31f;NPM使用场景&#x1f31f;NPM的常用命令&#x1f31f;NPM命令使用介绍&#x1f31f; 使用NPM安装模块&#x1f31f; 下载三方包&#x1f31f; 全局安装VS本地安装&#x1f31f; 本地安装&#x1f31f; 全局安装&…...

【2023最新】超详细图文保姆级教程:App开发新手入门(2)

上章节我们已经成功的创建了一个 App 项目&#xff0c;接下来我们讲述一下&#xff0c;如何导入项目、编辑代码和提交项目代码。 Let’s Go! 4. 项目导入 当用户创建一个新的应用时&#xff0c;YonStudio 开发工具会自动导入模板项目的默认代码&#xff0c;不需要手动进行代…...

sftp使用

Client端使用Server端的账户username&#xff0c;sftp登录Server&#xff0c;除了IP地址&#xff0c;也可以使用/etc/hosts定义的域名&#xff0c;注意&#xff0c;Client的默认路径&#xff1a;Shell中的当前路径&#xff0c;Server的默认路径&#xff1a;server账户家目录 ​…...