仿牛客论坛项目

news2025/8/2 11:23:53

源代码:https://gitee.com/qiuyusy/community

1. Spring

在测试类中使用Spring环境

  1. @RunWith(SpringRunner.class) 让Spring先运行
  2. @ContextConfiguration 导入配置文件
  3. implements ApplicationContextAware 后实现方法 获得 applicationContext
@RunWith(SpringRunner.class)
@SpringBootTest
@ContextConfiguration(classes = CommunityApplication.class)
class CommunityApplicationTests implements ApplicationContextAware {
    private ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    @Test
    public void testApplicationContext() {
        System.out.println(applicationContext);
        String[] beanDefinitionNames = applicationContext.getBeanDefinitionNames();
        for (String beanDefinitionName : beanDefinitionNames) {
            System.out.println(beanDefinitionName);
        }
    }
}

实际开发中没必要这样,直接@Autowired自动装配进来就行

比如

@SpringBootTest
class CommunityApplicationTests {

    @Autowired
    SimpleDateFormat simpleDateFormat;

    @Test
    void contextLoads() {
        System.out.println(simpleDateFormat.format(new Date()));
    }
}

@Primary的作用

如果使用接口获取Bean,这个接口下有多个实现类就会报错

AlphaDao alphaDao = applicationContext.getBean(AlphaDao.class);

可以使用@Primary指定使用哪个是实现类

image-20230115231111578

但是如果我某些地方还是想使用Hibernate的实现类怎么办,可以定义name来解决

@Repository("alphaHibernate")
public class AlphaDaoHibernateImpl implements AlphaDao {
    @Override
    public String select() {
        return "Hibernate";
    }
}
AlphaDao alphaDao = applicationContext.getBean("alphaHibernate", AlphaDao.class);
AlphaDao alphaDao = (AlphaDao) applicationContext.getBean("alphaHibernate");

实际开发中没必要用@Primary 直接 @Qualifier(“alphaHibernate”)指定就行了

class CommunityApplicationTests {

    @Autowired
    @Qualifier("alphaHibernate")
    AlphaDao alphaDao;

    @Test
    public void test(){
        System.out.println(alphaDao.select());
    }
}

@PostConstruct @PreDestroy

@Service
public class AlphaService {
    public AlphaService() {
        System.out.println("执行构造方法");
    }

    @PostConstruct
    public void init(){
        System.out.println("初始化...");
    }

    @PreDestroy
    public void destroy(){
        System.out.println("销毁");
    }
}

@PostConstruct 在构造器之后调用方法

@PreDestroy 在对象销毁前调用方法

image-20230115232447051

导入外部包的Bean

使用配置类来注入Bean

package com.qiuyu.config;

@Configuration
public class AlphaConfig {
    @Bean
    public SimpleDateFormat simpleDateFormat(){
        return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    }
}

2. SpringMVC

2.1 HTTP

https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Overview#http_%E6%B5%81

当客户端想要和服务端进行信息交互时(服务端是指最终服务器,或者是一个中间代理),过程表现为下面几步:

  1. 打开一个 TCP 连接

  2. 发送一个 HTTP 报文

    GET / HTTP/1.1
    Host: developer.mozilla.org
    Accept-Language: fr
    
  3. 读取服务端返回的报文信息:

    HTTP/1.1 200 OK
    Date: Sat, 09 Oct 2010 14:28:02 GMT
    Server: Apache
    Last-Modified: Tue, 01 Dec 2009 20:18:22 GMT
    ETag: "51142bc1-7449-479b075b2891b"
    Accept-Ranges: bytes
    Content-Length: 29769
    Content-Type: text/html
    
    <!DOCTYPE html... (here comes the 29769 bytes of the requested web page)
    
  4. 关闭连接或者为后续请求重用连接。

请求

image-20230115235447974

响应

image-20230115235504923

2.2 SpringMVC原理

这里的Front Controller 前端控制器其实就是DispatcherServlet

image-20230115235828307

2.3 Thymeleaf

image-20230116000134252

发送ModelAndView给模板

//第一种ModelAndView
@RequestMapping(value = "/teacher", method = RequestMethod.GET)
public ModelAndView getTeacher(){
    ModelAndView modelAndView = new ModelAndView();
    modelAndView.addObject("name", "qiuyu");
    modelAndView.addObject("age", 18);
    modelAndView.setViewName("demo/teacher");
    return modelAndView;
}

//第二种Model
@RequestMapping(value = "/teacher", method = RequestMethod.GET)
public String getTeacher(Model model){
    model.addAttribute("name", "qiuyu");
    model.addAttribute("age", 19);
    return "demo/teacher";
}

模板读取数据然后写入html,记得放在templates下

image-20230116004612588

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <p th:text="${name}"> </p>
    <p th:text="${age}"> </p>
</body>
</html>

image-20230116004549489

3. Mybatis-Plus

4. 开发社区首页

分页工具类

主要目的是为了加入路径,让前端的分页更好的复用

/**
 * 我的分页组件
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class MyPage<T> extends Page<T> {
    /**
     * 分页跳转的路径
     */
    protected String path;

}

4.1 Dao

直接Mybatis-Plus生成

@Mapper
public interface DiscussPostMapper extends BaseMapper<DiscussPost> {
}

@Mapper
public interface UserMapper extends BaseMapper<User> {
}

4.2 Service

package com.qiuyu.service;

@Service
public class DiscussPostService {
    @Autowired
    private DiscussPostMapper discussPostMapper;

    /**
     * 查询不是被拉黑的帖子,并且userId不为0按照type排序
     * @param userId
     * @Param page
     * @return
     */
    public IPage<DiscussPost> findDiscussPosts(int userId, IPage<DiscussPost> page) {
        LambdaQueryWrapper<DiscussPost> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper
                .ne(DiscussPost::getStatus, 2)
                .eq(userId != 0, DiscussPost::getUserId, userId)
                .orderByDesc(DiscussPost::getType, DiscussPost::getCreateTime);
        discussPostMapper.selectPage(page, queryWrapper);
        return page;
    }

    /**
     * userId=0查所有;userId!=0查个人发帖数
     *
     * @param userId
     * @return
     */
    public int findDiscussPostRows(int userId) {
        LambdaQueryWrapper<DiscussPost> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper
                .ne(DiscussPost::getStatus, 2)
                .eq(userId != 0, DiscussPost::getUserId, userId);
        int nums = discussPostMapper.selectCount(queryWrapper);
        return nums;
    }
}

package com.qiuyu.service;

@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    public User findUserById(String id) {
        return userMapper.selectById(Integer.parseInt(id));
    }
}

4.3 Controller

这里查询贴子,找到的只是userid,所以需要用userid找出user

采用Map的形式是为了之后Redis更方便

@GetMapping("/index")
public String getIndexPage(Model model, MyPage<DiscussPost> page) {
    page.setSize(10);
    page.setPath("/index");
    //查询到分页的结果
    page = (MyPage<DiscussPost>) discussPostService.findDiscussPosts(0, page);

    List<DiscussPost> list = page.getRecords();
    //因为这里查出来的是userid,而不是user对象,所以需要重新查出user
    List<Map<String, Object>> discussPorts = new ArrayList<>();
    if (list != null) {
        for (DiscussPost post : list) {
            Map<String, Object> map = new HashMap<>(15);
            map.put("post", post);
            User user = userService.findUserById(post.getUserId());
            map.put("user", user);
            discussPorts.add(map);
        }
    }

    model.addAttribute("discussPorts", discussPorts);
    model.addAttribute("page", page);

    return "/index";
}

按理说MyPage会自动放入model中,但是这里得手动加入,否则前端读取不到,不知道为啥

4.4 前端页面设计(Thymeleaf)

@表示前面加个项目路径(/community)

先导入thymeleaf<html lang="en" xmlns:th="http://www.thymeleaf.org">

<!doctype html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="icon" href="https://static.nowcoder.com/images/logo_87_87.png"/>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" crossorigin="anonymous">
<link rel="stylesheet" href="css/global.css" />
<title>牛客网-首页</title>

注意,因为把网页划分到了static和templates中,相对路径可能会找不到,可以加th解决,意思为让其到static下找资源
下面的js也要这么处理

<link rel="stylesheet" th:href="@{/css/global.css}" />

<script th:src="@{/js/global.js}"></script>
<script th:src="@{/js/index.js}"></script>

接受Model中的数据,并循环显示

th:each 循环遍历

<li th:each="map:${discussPorts}">
  • th:each用于循环输出数据
  • ${discussPorts} discussPorts为Model传过来的数据名称
  • map为在这里使用的属性名
<li th:each="i:${#numbers.sequence(page.current-2,page.current+2)}">
	<a th:text="${i}" th:if="${i} > 0 and ${i} <= ${page.pages} "></a>
</li>
  • #numbers.sequence 按照给定的开始和结束的数,从开始循环到结束
<p th:text="mapStat.count"></p
  • mapStat.count 属性名+Stat.count获取当前循环到第几个

th:src

<img th:src="${map.user.headerUrl}" >
  • ${map.user.headerUrl} 拿到map中的user属性的headerUrl属性

th:utext th:text

<a href="#" th:utext="${map.post.title}">
  • ${map.post.title}拿到map中的post属性的title属性
  • th:utext 会将转义字符转义
  • th:text 不会转义,直接输出

th:if 判断

<span  th:if="${map.post.type==1}">置顶</span>
  • 如果map.post.type 等于 1 才显示置顶

时间格式化

使用dates工具格式化

<b th:utext="${#dates.format(map.post.createTime,'yyyy-MM-dd HH:mm:ss')}">

获取list长度

<p th:text="${#lists.size(discussPorts)}"></p>

th:href 前端向后端发送带参请求

<a th:href="@{/community/index(currentPage=1,pageSize=10)}">首页</a>
<a th:href="@{${path}(currentPage=${page.getPages()},pageSize=10)}">末页</a>

th:class class样式判断

先用| |把class内括起来 然后写判断

<li th:class="|page-item ${page.current==1?'disabled':''}|">

th:value

设置默认值

<input type="text" th:value="${user!=null ? user.username : ''}"
   id="username" name="username" placeholder="请输入您的账号!" required>

分页

<!-- 分页 -->
<nav class="mt-5" th:if="${page.pages > 1}" th:fragment="pagination">
    <ul class="pagination justify-content-center">
        <li class="page-item">
            <a class="page-link" th:href="@{${page.path}(current=1)}">首页</a>
        </li>
        <li th:class="|page-item ${page.current==1?'disabled':''}|">
            <a class="page-link"  th:href="@{${page.path}(current=${page.current}-1)}">上一页</a>
        </li>

        <li th:class="|page-item ${i==page.current?'active':''}|" th:each="i:${#numbers.sequence(page.current-2,page.current+2)}">
            <a class="page-link" th:href="@{${page.path}(current=${i})}" th:text="${i}" th:if="${i} > 0 and ${i} <= ${page.pages} "></a>
        </li>

        <li class="page-item" th:class="|page-item ${page.current==page.pages?'disabled':''}|">
            <a class="page-link" th:href="@{${page.path}(current=${page.current}+1)}">下一页</a>
        </li>
        <li class="page-item">
            <a class="page-link" th:href="@{${page.path}(current=${page.pages})}">末页</a>
        </li>
    </ul>
</nav>

5. 开发注册功能

5.1 项目调试

状态码

  • 200 成功
  • 302 重定向
    返回302和一个url,建议你去访问这个url
    比如你删除完,想查询一下,就可以在删除后重定向到查询页,降低耦合
  • 400 请求参数有误
  • 403 服务器拒绝执行请求
  • 404 not found
  • 500 服务器遇到不知道如何处理的情况

5.2 日志

trace>debug>info>warn>error

level下写的是包,填写最低显示级别

#logger
logging:
  level:
    com.qiuyu: warn

然后创建一个Logger就行(注意是 org.slf4j.Logger )

@SpringBootTest
@RunWith(SpringRunner.class)
public class LoggerTest {
    private static final Logger logger = LoggerFactory.getLogger(LoggerTest.class);

    @Test
    public void testLogger(){
        logger.debug("debug");
        logger.info("info");
        logger.warn("warn");
        logger.error("error");;
    }
}

存储到文件

简单

logging:
  level:
    com.qiuyu: debug
  file:
    name: community.log

复杂

使用logback-spring.xml配置

5.3 发送邮件

如何发邮件

邮箱需要开启SMTP协议

image-20230117161549865

导入jar包

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-mail</artifactId>
</dependency>

配置

spring: 
  # 邮箱
  mail:
    #配置邮件消息
    host: smtp.qq.com
    port: 465
    #发送邮件者信箱(也就是你申请POP3/SMTP服务的QQ号)
    username: ***@qq.com
    #申请PO3/SMTP服务时,给我们的邮箱的授权码
    password: *****
    default-encoding: UTF-8
    protocol: smtp
    properties:
      mail.smtp.ssl.enable: true

普通邮件

写一个工具类用于发邮件

package com.qiuyu.utils;

@Component
public class MailClient {
    private static final Logger logger = LoggerFactory.getLogger(MailClient.class);

    @Autowired
    private JavaMailSender mailSender;

    /**
     * 从yml中读取发件人
     */
    @Value("${spring.mail.username}")
    private String from;

    public void sendMail(String to, String subject, String content) {
        try {
            MimeMessage message = mailSender.createMimeMessage();
            MimeMessageHelper helper = new MimeMessageHelper(message);
            //设置收发件人
            helper.setFrom(from);
            helper.setTo(to);
            //设置邮件
            helper.setSubject(subject);
            helper.setText(content,true); //true表示支持html格式
            //发送
            mailSender.send(helper.getMimeMessage());
        } catch (MessagingException e) {
            logger.error("发送邮件失败" + e.getMessage());
        } finally {
        }
    }
}

@Test
public void testSendMail() {
    String to = "****@qq.com";
    String subject = "测试邮件";
    String content = "测试邮件内容";
    mailClient.sendMail(to, subject, content);
}

使用Thymeleaf模板发邮件

写一个模板

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>邮箱示例</title>
</head>
<body>
    <p>欢迎你,<span style="color: red" th:text="${username}"></span></p>
</body>
</html>

发送邮件

package com.qiuyu.utils;

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.test.context.junit4.SpringRunner;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;

@SpringBootTest
@RunWith(SpringRunner.class)
public class MailClientTest {
    @Autowired
    private MailClient mailClient;

    @Autowired
    private TemplateEngine templateEngine;

    @Test
    public void testSendHtmlMail() {
        String to = "2448567284@qq.com";
        String subject = "测试邮件";

        //创建数据
        Context context = new Context();
        context.setVariable("username", "qiuyu");

        //根据模板,放入数据
        String content = templateEngine.process("/mail/demo", context);
        System.out.println(content);

        //发送
        mailClient.sendMail(to, subject, content);
    }
}

5.4 注册实现

image-20230117171832699

1. 访问注册页面

@Controller
public class LoginController {
    @GetMapping("/register")
    public String getRegisterPage() {
        return "/site/register";
    }
}

复用头部header

index.html

<header class="bg-dark sticky-top" th:fragment="header">
  • th:fragment="header" 被复用的部分,取名header

register.html

<header class="bg-dark sticky-top" th:replace="index::header"></header>
  • th:replace="index::header" 复用index的header

2. 编写工具类

导入一个字符串处理工具类依赖

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
</dependency>

配置一下域名,让邮箱访问(key可以自定义)

# community
community:
  path:
    domain: http://localhost:80

写一个工具类,包括获得随机字符串和md5加密

密码在存入数据库时,需要进行md5加密
但是如果简单密码经过md5加密后,也可能会被黑客撞库攻击

所以先将密码进行加盐(salt)后再进行md5加密

package com.qiuyu.utils;

import org.apache.commons.lang3.StringUtils;
import org.springframework.util.DigestUtils;

import java.util.UUID;

public class CommunityUtil {
    /*
     * 生成随机字符串
     * 用于邮件激活码,salt5位随机数加密
     **/
    public static String generateUUID(){
        return UUID.randomUUID().toString().replaceAll("-","");
    }
    /* MD5加密
     * hello-->abc123def456
     * hello + 3e4a8-->abc123def456abc
     */
    public static String md5(String key){
        //检查时候为null 空 空格 
        if (StringUtils.isBlank(key)){
            return null;
        }
        //MD5加密方法
        return DigestUtils.md5DigestAsHex(key.getBytes());
        //参数是bytes型
    }
}

还有常量接口(实现接口使用)

package com.qiuyu.utils;

/**
 * @author QiuYuSY
 * @create 2023-01-17 22:06
 * 一些常量
 */
public interface CommunityConstant {
    /*      以下用于注册功能      */
    /** 激活成功*/
    int ACTIVATION_SUCCESS=0;
    /** 重复激活 */
    int ACTIVATION_REPEAT=1;
    /** 激活失败 */
    int ACTIVATION_FAILURE=2;


    /*      以下用于登录功能*      /
    /**
     * 默认状态的登录凭证的超时时间
     */
    int DEFAULT_EXPIRED_SECONDS=3600*12;
    /**
     * 记住状态的登录凭证超时时间
     */
    int REMEMBER_EXPIRED_SECONDS=3600*24*7;
}

3. 编写Service层

输入合法性验证

  1. 输入user对象不为null
  2. 输入各项属性不为空(使用字符串工具类)
  3. 用户名邮箱是否已被注册

注册账户

  1. 设置salt加密(随机5位数加入密码)
  2. 设置密码+salt
  3. 设置UUID随机数激活码
  4. 初始化status,type=0,时间
  5. 设置头像(动态)

发送邮件

  1. 创建Context对象–>context.setVariable(name,value)将name传入前端,为thymeleaf提供变量
  2. 设置email和url
  3. templateEngine.process执行相应HTML
  4. 发送邮件
public Map<String,Object> register(User user){
    Map<String,Object> map = new HashMap<>();

    //空值处理
    if (user == null) {
        throw new IllegalArgumentException("参数不能为空");
    }
    if(StringUtils.isBlank(user.getUsername())){
        map.put("usernameMsg", "账号不能为空");
        return map;
    }
    if(StringUtils.isBlank(user.getPassword())){
        map.put("passwordMsg", "密码不能为空");
        return map;
    }
    if(StringUtils.isBlank(user.getEmail())){
        map.put("emailMsg", "邮箱不能为空");
        return map;
    }

    //判断账号是否被注册
    Integer integer = userMapper.selectCount(new LambdaQueryWrapper<User>().eq(User::getUsername, user.getUsername()));
    if(integer > 0){
        map.put("usernameMsg", "该账号已被注册");
        return map;
    }
    //判断邮箱是否被注册
    integer = userMapper.selectCount(new LambdaQueryWrapper<User>().eq(User::getEmail, user.getEmail()));
    if(integer > 0){
        map.put("emailMsg", "该邮箱已被注册");
        return map;
    }


    //给用户加盐
    user.setSalt(CommunityUtil.generateUUID().substring(0,5));
    //加密
    user.setPassword(CommunityUtil.md5(user.getPassword() + user.getSalt()));
    //初始化其他数据
    user.setType(0);
    user.setStatus(0);
    user.setActivationCode(CommunityUtil.generateUUID());
    user.setHeaderUrl(String.format("http://images.nowcoder.com/head/%dt.png", new Random().nextInt(1000)));
    user.setCreateTime(new Date());
    //注册用户
    userMapper.insert(user);


    //激活邮件
    //创建数据
    Context context = new Context();
    context.setVariable("email", user.getEmail());
    //http://localhost:8080/community/activation/101/code 激活链接
    String url = domain + contextPath + "/activation/"+ user.getId()+"/" + user.getActivationCode();
    context.setVariable("url", url);

    //根据模板,放入数据
    String content = templateEngine.process("/mail/activation", context);
    //发送
    mailClient.sendMail(user.getEmail(), "激活账号", content);

	//map为空则注册成功
    return map;
}

激活邮箱

/**
     * 激活账号
     * @param userId
     * @param activationCode
     * @return
     */
public int activate(int userId, String activationCode) {
    //根据userid获取用户信息
    User user = userMapper.selectById(userId);

    if(user.getStatus() == 1){
        //已经激活,则返回重复
        return ACTIVATION_REPEAT;
    } else if (user.getActivationCode() .equals(activationCode)) {
        //如果未激活,判断激活码是否相等
        //激活账号
        user.setStatus(1);
        userMapper.updateById(user);
        return ACTIVATION_SUCCESS;
    } else {
        //不相等
        return ACTIVATION_FAILURE;
    }
}

4. 编写Controller层

  1. 如果注册成功,则到一个中转页面
  2. 不成功则把失败消息传到注册页面,重新注册
package com.qiuyu.controller;


@Controller
public class LoginController {

    @Autowired
    private UserService userService;

    /**
     * 跳转到请求页面
     * @return
     */
    @GetMapping("/register")
    public String getRegisterPage() {
        return "/site/register";
    }

    /**
     * 注册账号,发送邮箱
     */
    @PostMapping("/register")
    public String register(Model model, User user) {
        Map<String, Object> map = userService.register(user);

        if(map == null || map.isEmpty()){
            //注册成功,跳转到中转页面
            model.addAttribute("msg","注册成功,我们已经向您的邮件发送了一封激活邮件,请尽快激活!");
            model.addAttribute("target","/index");
            return "/site/operate-result";
        }else{
            //注册失败,重新注册
            model.addAttribute("usernameMsg",map.get("usernameMsg"));
            model.addAttribute("passwordMsg",map.get("passwordMsg"));
            model.addAttribute("emailMsg",map.get("emailMsg"));
            return "/site/register";
        }
    }
}

激活邮箱

/**
 * 激活邮箱 http://localhost:8080/community/activation/101/code 激活链接
 * @param model
 * @param userId
 * @param code
 * @return
 */
@GetMapping("/activation/{userId}/{code}")
public String activate(Model model,
                       @PathVariable("userId") int userId,
                       @PathVariable("code") String code) {
    int result = userService.activate(userId, code);
    if (result == ACTIVATION_SUCCESS){
        model.addAttribute("msg","激活成功,你的账号已经可以正常使用了!");
        model.addAttribute("target","/login");
    }else if (result == ACTIVATION_REPEAT){
        model.addAttribute("msg","无效操作,该账号已经激活过了!");
        model.addAttribute("target","/index");
    }else {
        model.addAttribute("msg","激活失败,你提供的激活码不正确!");
        model.addAttribute("target","/index");
    }
    return "/site/operate-result";
}

6. 开发登录功能

6.1 会话

image-20230118000123296

Cookie

密码之类隐私数据用Cookie放浏览器不安全

@GetMapping("/cookie/set")
@ResponseBody
public String setCookie(HttpServletResponse response) {
    //创建Cookie
    Cookie cookie = new Cookie("code", CommunityUtil.generateUUID());
    //设置Cookie生效范围
    cookie.setPath("/community");
    //设置cookie有效时间(s)
    cookie.setMaxAge(60 * 10);
    //发送Cookie
    response.addCookie(cookie);

    return "setCookie";
}

@GetMapping("/cookie/get")
@ResponseBody
public String getCookie(@CookieValue("code") String code) {
    
    return code;
}

Session

服务器把sessionId用cookie给浏览器,浏览器只存了sessionId

缺点是耗费内存

image-20230118000610740

Set-Cookie: JSESSIONID=71B1E0DDFA9BD595C5E7F584AD56E7F6; Path=/community; HttpOnly

  • 存的是sessionID
@GetMapping("/session/set")
@ResponseBody
public String setSession(HttpSession session) {
    session.setAttribute("id",1);
    session.setAttribute("name","Test");
    session.setAttribute("pwd","ASDASDDADASD");
    return "setSession";
}

@GetMapping("/session/get")
@ResponseBody
public String getSession(HttpSession session) {
    System.out.println(session.getAttribute("id"));
    System.out.println(session.getAttribute("name"));
    System.out.println(session.getAttribute("pwd"));
    return "getSession";
}

为什么Session在分布式情况下尽量少使用

因为负载均衡无法保证同个用户的多次请求都能到同一台服务器,而session只在第一次请求的服务器中

解决方案

  1. 让用户的多次请求粘性的访问同一台服务器
    • 缺点:破环了负载均衡
  2. 让每个服务器都同步一份session
    • 缺点:耗费内存,耦合高
  3. 额外专门设置一台服务器用于存放session,其他服务器来这台服务器取session
    • 缺点:如果这台存放session的服务器挂了就gg,而如果用session集群架设那又和方案二一样了
  4. 使用数据库集群(Ridds)

6.2 验证码

参考网站 :http://code.google.com/archive/p/kaptcha/

注意:

1.Producer是Kaptcha的核心接口

2.DefaultKaptcha是Kaptcha核心接口的默认实现类

3.Spring Boot没有为Kaptcha提供自动配置

导入

<dependency>
   <groupId>com.github.penggle</groupId>
   <artifactId>kaptcha</artifactId>
   <version>2.3.2</version>
</dependency>

配置

package com.qiuyu.config;

import com.google.code.kaptcha.Producer;
import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.google.code.kaptcha.util.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Properties;

@Configuration
public class KaptchaConfig {
    @Bean
    public Producer KaptchaProducer(){
        /**
         * 手动创建properties.xml配置文件对象*
         * 设置验证码图片的样式,大小,高度,边框,字体等
         */
        Properties properties=new Properties();
        properties.setProperty("kaptcha.border", "yes");
        properties.setProperty("kaptcha.border.color", "105,179,90");
        properties.setProperty("kaptcha.textproducer.font.color", "black");
        properties.setProperty("kaptcha.image.width", "110");  //宽度
        properties.setProperty("kaptcha.image.height", "40");  //高度
        properties.setProperty("kaptcha.textproducer.font.size", "32"); //字号
        properties.setProperty("kaptcha.textproducer.char.string", "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ");
        properties.setProperty("kaptcha.textproducer.char.length", "4"); //几个字符
        properties.setProperty("kaptcha.textproducer.font.names", "宋体,楷体,微软雅黑");
        properties.setProperty("kaptcha.noise.impl", "com.google.code.kaptcha.impl.NoNoise"); //是否干扰


        DefaultKaptcha Kaptcha=new DefaultKaptcha();
        Config config=new Config(properties);
        Kaptcha.setConfig(config);

        return Kaptcha;
    }

}

使用(注意生成的文本要放入session,等待验证用户的输入)

@GetMapping("/kaptcha")
public void getKaptcha(HttpServletResponse response, HttpSession session){
    //生成验证码
    String text = kaptchaProducer.createText();
    BufferedImage image = kaptchaProducer.createImage(text);

    //验证码存入session,用于验证用户输入是否正确
    session.setAttribute("kaptcha",text);

    //将图片输出到浏览器
    response.setContentType("image/png");
    try {
        OutputStream os = response.getOutputStream();
        ImageIO.write(image,"png",os);
        os.flush();
    } catch (IOException e) {
        logger.error("响应验证码失败:"+e.getMessage());
    }
}

刷新验证码

HTML

<img th:src="@{/kaptcha}" id="kaptcha" style="width:100px;height:40px;" />
<a href="javascript:refresh_kaptcha();">刷新验证码</a>
						

JS

有些浏览器认为图片为静态资源,地址没变,就不刷新,带个参数可以解决

<script>
    function refresh_kaptcha(){
        //用?带个参数欺骗浏览器,让其认为是个新路径
        var path = CONTEXT_PATH + "/kaptcha?p=" + Math.random();
        $("#kaptcha").attr("src", path);
	}
</script>

6.3 登录实现

image-20230118140643375

登录后需要使用cookie或session进行登录凭证的验证,但是上面说到了这两种方案的缺点

  • cookie不安全
  • session耗资源,分布式不适合

这里使用把凭证存入数据库的方式,先存入mysql,后续转redis

Bean

package com.qiuyu.bean;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginTicket {
    private Integer id;
    private Integer userId;
    private String ticket;
    private Integer status;
    private Date expired;

}

DAO

MyBatis-Plus生成

Service

  1. 空值判断
  2. 根据username查找user,判断是否存在该用户
  3. 对输入密码进行加盐如何md5加密,然后进行比对
  4. 写入登录凭证到数据库
package com.qiuyu.service;

@Service
public class LoginService {
    @Autowired
    private LoginTicketMapper loginTicketMapper;

    @Autowired
    private UserMapper userMapper;

    /**
     * 登录
     * @param username
     * @param password
     * @param expiredSeconds
     * @return
     */
    public Map<String,Object> login(String username, String password, int expiredSeconds){
        HashMap<String, Object> map = new HashMap<>();

        //空值处理
        if (StringUtils.isBlank(username)) {
            map.put("usernameMsg","用户名不能为空");
            return map;
        }
        if (StringUtils.isBlank(password)) {
            map.put("passwordMsg","密码不能为空");
            return map;
        }

        //验证账号是否存在
        User user = userMapper.selectOne(new LambdaQueryWrapper<User>()
                .eq(User::getUsername, username));
        if(user == null){
            map.put("usernameMsg","该账号不存在");
            return map;
        }

        //验证激活状态
        if(user.getStatus() == 0){
            map.put("usernameMsg","该账号未激活");
            return map;
        }

        //验证密码(先加密再对比)
        String pwdMd5 = CommunityUtil.md5(password + user.getSalt());
        if(!pwdMd5.equals(user.getPassword())){
            map.put("passwordMsg","密码错误");
            return map;
        }

        //生成登录凭证(相当于记住我这个功能==session)
        LoginTicket ticket = new LoginTicket();
        ticket.setUserId(user.getId());
        ticket.setTicket(CommunityUtil.generateUUID());
        ticket.setStatus(0); //有效
        //当前时间的毫秒数+过期时间毫秒数
        ticket.setExpired(new Date(System.currentTimeMillis() + expiredSeconds * 1000));
        Date date = new Date();

        loginTicketMapper.insert(ticket);

        map.put("ticket",ticket.getTicket());
        //map中能拿到ticket说明登录成功了
        return map;
    }
}

Controller

  1. 判断验证码是否正确
  2. 判断登录是否成功
  3. 成功就发送一个带有登录凭证的cookie给浏览器
  4. 不成功就重新登录
/**
 * 登录功能
 * @param username
 * @param password
 * @param code 验证码
 * @param rememberme 是否勾选记住我
 * @param model
 * @param session 用于获取kaptcha验证码
 * @param response 用于浏览器接受cookie
 * @return
 */
@PostMapping(path = "/login")
public String login(String username, String password, String code, boolean rememberme,
                    Model model, HttpSession session, HttpServletResponse response){
    //判断验证码
    String kaptcha = (String) session.getAttribute("kaptcha");
    if(StringUtils.isBlank(kaptcha) || StringUtils.isBlank(code) || !kaptcha.equalsIgnoreCase(code)){
        //空值或者不相等
        model.addAttribute("codeMsg","验证码不正确");
        return "site/login";
    }

    /*
     * 1.验证用户名和密码(重点)
     * 2.传入浏览器cookie=ticket
     */
    int expiredSeconds=rememberme?REMEMBER_EXPIRED_SECONDS:DEFAULT_EXPIRED_SECONDS;
    Map<String, Object> map = loginService.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";
    }

}

前端

输入错误保留之前的输入

<input type="text"  name="username"
      th:value="${param.username}"
      id="username" placeholder="请输入您的账号!" required>
<input type="checkbox" name="rememberme" id="remember-me"
      th:checked="${param.rememberme}">
  • 注意Controller中username和password并没有放入到model中,所以model中没有这俩,有两种方案
    1. 手动把username和password放入到model中
    2. 直接在request中取 在th中 为param

输入错误的提示

<input type="password" name="password"
      th:class="|form-control ${passwordMsg!=null?'is-invalid':''}|"
      th:value="${param.password}"
      id="password" placeholder="请输入您的密码!" required>
<div class="invalid-feedback" th:text="${passwordMsg}">
   密码长度不能小于8位!
</div> 
  • 判断是否有msg,有的话才加上is-invalid样式显示提示

6.4 登出实现

Service层

/**
 *  登出
 * @param ticket 登录凭证
 */
public void logout(String ticket){
    LoginTicket loginTicket = new LoginTicket();
    loginTicket.setStatus(1);
    loginTicketMapper.update(loginTicket,
                             new LambdaUpdateWrapper<LoginTicket>().eq(LoginTicket::getTicket,ticket));
}

Controller层

/**
* 退出登录功能
* @CookieValue()注解:将浏览器中的Cookie值传给参数
*/
@GetMapping("/logout")
public String logout(@CookieValue("ticket") String ticket){
    userService.logout(ticket);
    return "redirect:/login";//重定向
}
  • @CookieValue 将浏览器中的Cookie值传给参数

6.5 显示登录信息

image-20230118164557688

拦截器demo

  1. 拦截器需实现HandlerInterceptor接口而配置类需实现WebMvcConfigurer接口。
  2. preHandle方法在Controller之前执行,若返回false,则终止执行后续的请求。
  3. postHandle方法在Controller之后、模板页面之前执行。
  4. afterCompletion方法在模板之后执行。
  5. 通过addInterceptors方法对拦截器进行配置

1. 创建拦截器类,实现HandlerInterceptor接口

  • handle就是在执行的方法,也就是拦截的目标
@Component
public class AlphaInterceptor implements HandlerInterceptor {
    private static final Logger logger = LoggerFactory.getLogger(AlphaInterceptor.class);
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        logger.debug("preHandle");
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        logger.debug("postHandle");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        logger.debug("afterCompletion");
    }
}

2. 创建拦截器配置类,实现WebMvcConfigurer接口

package com.qiuyu.config;

import com.qiuyu.controller.interceptor.AlphaInterceptor;
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("/**/*.css","/**/*.js","/**/*.png","/**/*.jpg","/**/*.jpeg") //不拦截静态资源
                .addPathPatterns("/register","/login"); //只拦截部分请求
    }
}

1. 首先创建两个工具类降低耦合

Request获取Cookie工具类,获取凭证ticket多线程工具类

package com.qiuyu.utils;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;


public class CookieUtil {
    /**
     * 从request中获取指定cookie对象
     * @param request
     * @param name
     * @return
     */
    public static String getValue(HttpServletRequest request, String name){
        if (request==null||name==null){
            throw new IllegalArgumentException("参数为空!");
        }
        Cookie[] cookies = request.getCookies();
        if (cookies!=null){
            for (Cookie cookie : cookies){
                if (cookie.getName().equals(name)){
                    return cookie.getValue();
                }
            }
        }
        return null;
    }
}

注意:

  1. ThreadLocal采用线程隔离的方式存放数据,可以避免多线程之间出现数据访问冲突。

  2. ThreadLocal提供set方法,能够以当前线程为key存放数据。get方法,能够以当前线程为key获取数据。

  3. ThreadLocal提供remove方法,能够以当前线程为key删除数据。

因为用户登录后,需要把用户信息放入内存之中,而web时多线程的环境,每个用户都会有一个线程

为了避免线程之间干扰,需要采用ThreadLocal进行线程隔离

package com.qiuyu.utils;

import com.qiuyu.bean.User;
import org.springframework.stereotype.Component;

/**
 * 持有用户信息,代替session对象
 */
@Component  //放入容器里不用设为静态方法
public class HostHolder {
    //key就是线程对象,值为线程的变量副本
    private ThreadLocal<User> users = new ThreadLocal<>();

    /**
     * 以线程为key存入User
     * @param user
     */
    public void setUser(User user){
        users.set(user);
    }

    /**
     * 从ThreadLocal线程中取出User
     * @return
     */
    public User getUser(){
        return users.get();
    }

    /**
     * 释放线程
     */
    public void clear(){
        users.remove();
    }
}

2. DAO层

 /**
 * 通过凭证号找到凭证
 * @param ticket
 * @return
 */
 public LoginTicket findLoginTicket(String ticket){
 return loginTicketMapper.selectOne(new LambdaQueryWrapper<LoginTicket>()
 .eq(LoginTicket::getTicket, ticket));

}

3. 创建登录凭证拦截器(等同于Controller层)

  1. preHandle: 在进入controller之前,把请求拦下,判断是否有凭证,有的话根据凭证查出用户,存入ThreadLocal
  2. postHandle:controller处理完之后,到视图之前,把ThreadLocal中的用户存入ModelAndView给前端调用
  3. afterCompletion: 最后把ThreadLocal中的当前user删除
package com.qiuyu.controller.interceptor;

/**
 * 登录凭证拦截器,用于根据凭证号获取用户,并传给视图
 */
@Component
public class LoginTicketInterceptor implements HandlerInterceptor {
    @Autowired
    private UserService userService;

    @Autowired
    private HostHolder hostHolder;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //从request中获取cookie 凭证
        String ticket = CookieUtil.getValue(request, "ticket");

        if (!StringUtils.isBlank(ticket)) {
            // 查询凭证
            LoginTicket loginTicket = userService.findLoginTicket(ticket);
            // 检查凭证是否有效
            if (loginTicket != null && loginTicket.getStatus() == 0 && loginTicket.getExpired().after(new Date())) {
                // 根据凭证查询用户
                User user = userService.findUserById(String.valueOf(loginTicket.getUserId()));
                // 把用户存入ThreadLocal
                hostHolder.setUser(user);
            }
        }
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        //在调用模板引擎之前,把user给model
        User user = hostHolder.getUser();
        if (user != null && modelAndView != null) {
            modelAndView.addObject("loginUser",user);
        }
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        //最后把ThreadLocal中的当前user删除
        hostHolder.clear();
    }
}

4. 编写拦截配置类

package com.qiuyu.config;
/**
 * 拦截器配置类
 */
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Autowired
    private LoginTicketInterceptor loginTicketInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginTicketInterceptor)
                .excludePathPatterns("/**/*.css","/**/*.js","/**/*.png","/**/*.jpg","/**/*.jpeg");
    }
}

5. 前端

<li class="nav-item ml-3 btn-group-vertical" th:if="${loginUser!=null}">
	<a href="site/letter.html">消息<span >12</span></a>
</li>
  • th:if="${loginUser!=null}" 如果有loginUser传过来才显示
  • th:if="${loginUser==null}" 如果有没有loginUser传过来才显示

6.6 拦截未登录页面访问(采用注解)

当前情况下,没登录也能够访问/user/setting,想要不让其访问,可以使用之前的那种拦截器,这里采用注解的方法

image-20230118222722899

常用的元注解:

  • @Target:注解作用目标(方法or类)
  • @Retention:注解作用时间(运行时or编译时)
  • @Document:注解是否可以生成到文档里
  • @Inherited**:注解继承该类的子类将自动使用@Inherited修饰

注意: 若有2个拦截器,拦截器执行顺序为注册在WebMvcConfig配置类中的顺序

1. 写一个注解@LoginRequired

package com.qiuyu.annotation;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginRequired {

}

2. 给需要的方法加上注解

/**
 * 跳转设置页面
 * @return
 */
@LoginRequired
@GetMapping("/setting")
public String getUserPage() {
    return "/site/setting";
}

/**
*上传头像
*/
@LoginRequired
@PostMapping("/upload")
public String uploadHeader(MultipartFile headerImage, Model model) {

}

3. 写拦截器

拦截有注解,并且没登陆的那些请求

package com.qiuyu.controller.interceptor;

/**
 * @LoginRequired的拦截器实现
 */
@Component
public class LoginRequiredInterceptor implements HandlerInterceptor {
    @Autowired
    private HostHolder hostHolder;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //判断拦截的是否为方法
        if (handler instanceof HandlerMethod) {
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            //获取拦截到的方法对象
            Method method = handlerMethod.getMethod();
            //获取注解
            LoginRequired loginRequired = method.getAnnotation(LoginRequired.class);
            //如果这个方法被@LoginRequired注解,并且未登录,跳转并拦截!
            if (loginRequired != null && hostHolder.getUser() == null) {
                response.sendRedirect(request.getContextPath()+"/login");
                return false;
            }
        }
        return true;
    }
}

4. 注册到拦截器配置类

package com.qiuyu.config;

/**
 * 拦截器配置类
 */

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Autowired
    private LoginTicketInterceptor loginTicketInterceptor;
    @Autowired
    private LoginRequiredInterceptor loginRequiredInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginTicketInterceptor)
                .excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg");

        registry.addInterceptor(loginRequiredInterceptor)
                .excludePathPatterns("/**/*.css","/**/*.js","/**/*.png","/**/*.jpg","/**/*.jpeg");
    }
}

7. 账号设置

image-20230118205703762

上传头像

注意:1. 必须是Post请求 2.表单:enctype=“multipart/form-data” 3.参数类型MultipartFile只能封装一个文件

上传路径可以是本地路径也可以是web路径

访问路径必须是符合HTTP协议的Web路径

Service层

/**
* 更新用户头像路径
* @param userId
* @param headerUrl
* @return
*/
public int updateHeaderUrl(int userId, String headerUrl) {
    User user = new User();
    user.setId(userId);
    user.setHeaderUrl(headerUrl);
    return userMapper.updateById(user);
}

Controller层

  • 把图像传入本地
    1. MultipartFile读取图片
    2. 对图片进行合法性判断,重命名图像
    3. 上传到指定位置,并指定url
    4. 修改用户的头像url
  • 从本地读取图像
    1. 根据url解析得到文件地址
    2. 用缓冲区读取图片,输出到输出流中
package com.qiuyu.controller;

@Controller
@RequestMapping("/user")
public class UserController {
    public static final Logger logger = LoggerFactory.getLogger(UserController.class);

    @Value("${community.path.domain}")
    private String domain;
    @Value("${community.path.upload-path}")
    private String uploadPath;
    @Value("${server.servlet.context-path}")
    private String contextPath;

    @Autowired
    private UserService userService;
    @Autowired
    private HostHolder hostHolder;

    /**
     * 上传头像
     *
     * @param headerImage
     * @param model
     * @return
     */
    @PostMapping("/upload")
    public String uploadHeader(MultipartFile headerImage, Model model) {
        if (headerImage == null) {
            model.addAttribute("error", "您还没有选择图片!");
            return "/site/setting";
        }

        /*
         * 获得原始文件名字
         * 目的是:生成随机不重复文件名,防止同名文件覆盖
         * 方法:获取.后面的图片类型 加上 随机数
         */
        String filename = headerImage.getOriginalFilename();
        int index = filename.lastIndexOf(".");
        String suffix = filename.substring( index+1);

        //任何文件都可以上传,根据业务在此加限制.这里为没有后缀不合法
        if (StringUtils.isBlank(suffix) || index < 0) {
            model.addAttribute("error", "文件格式不正确!");
            return "/site/setting";
        }

        //生成随机文件名
        filename = CommunityUtil.generateUUID() +"."+ suffix;

        //确定文件存放路径
        File dest = new File(uploadPath + "/" + filename);
        try {
            //将文件存入指定位置
            headerImage.transferTo(dest);
        } catch (IOException e) {
            logger.error("上传文件失败: " + e.getMessage());
            throw new RuntimeException("上传文件失败,服务器发生异常!", e);
        }


        //更新当前用户的头像的路径(web访问路径)
        //http://localhost:8080/community/user/header/xxx.png
        User user = hostHolder.getUser();
        String headerUrl = domain + contextPath + "/user/header/" + filename;
        userService.updateHeaderUrl(user.getId(), headerUrl);

        return "redirect:/index";
    }


    /**
     * 得到服务器图片
     * void:返回给浏览器的是特色的图片类型所以用void
     *
     * @param fileName
     * @param response
     */
    @GetMapping("/header/{fileName}")
    public void getHeader(@PathVariable("fileName") String fileName, HttpServletResponse response) {
        // 服务器存放路径(本地路径)
        fileName = uploadPath + "/" + fileName;
        // 文件后缀
        String suffix = fileName.substring(fileName.lastIndexOf(".") + 1);
        // 浏览器响应图片
        response.setContentType("image/" + suffix);
        try (
            //图片是二进制用字节流
            FileInputStream fis = new FileInputStream(fileName);
            OutputStream os = response.getOutputStream();
        ) {
            //设置缓冲区
            byte[] buffer = new byte[1024];
            //设置游标
            int b = 0;
            while ((b = fis.read(buffer)) != -1) {
                os.write(buffer, 0, b);
            }
        } catch (IOException e) {
            logger.error("读取头像失败: " + e.getMessage());
        }
    }
}

修改密码

image-20230118232307654
  1. 原密码加盐,md5加密
  2. 判断新密码密码和原密码是否相等
  3. 新密码加盐,md5加密
  4. 写入数据库(Service层)

Service层

/**
* 更新密码
* @param userId
* @param oldPassword
* @param newPassword
* @return map返回信息
*/
public Map<String, Object> updatePassword(int userId, String oldPassword,
                                          String newPassword) {
    Map<String, Object> map = new HashMap<>();

    //空值判断
    if(StringUtils.isBlank(oldPassword)){
        map.put("oldPasswordMsg","原密码不能为空");
        return map;
    }
    if(StringUtils.isBlank(newPassword)){
        map.put("newPasswordMsg","新密码不能为空");
        return map;
    }

    //根据userId获取对象
    User user = userMapper.selectById(userId);
    //旧密码加盐,加密
    oldPassword = CommunityUtil.md5(oldPassword+user.getSalt());
    //判断密码是否相等
    if(!user.getPassword().equals(oldPassword)){
        //不相等,返回
        map.put("oldPasswordMsg","原密码错误");
        return map;
    }

    //新密码加盐,加密
    newPassword = CommunityUtil.md5(newPassword+user.getSalt());

    user.setPassword(newPassword);
    userMapper.updateById(user);

    //map为空表示修改成功
    return map;
}

Controller层

从ThreadLocal中拿userid

/**
 *  更新密码
 * @param oldPassword
 * @param newPassword
 * @param model
 * @return
 */
@LoginRequired
@PostMapping("/updatePassword")
public String updatePassword(String oldPassword, String newPassword,Model model){
    User user = hostHolder.getUser();

    Map<String, Object> map =
            userService.updatePassword(user.getId(), oldPassword, newPassword);

    if(map == null || map.isEmpty()){
        //成功!使用重定向,不然报错
        return "redirect:/index";
    }else{
        //失败
        model.addAttribute("oldPasswordMsg",map.get("oldPasswordMsg"));
        model.addAttribute("newPasswordMsg",map.get("newPasswordMsg"));
        return "/site/setting";
    }
}

前端

<input type="password" th:class="|form-control ${oldPasswordMsg!=null?'is-invalid':''}|"
      th:value="${param.oldPassword!=null?param.oldPassword:''}"
      id="old-password" name="oldPassword" placeholder="请输入原始密码!" required>
  • th:value="${param.oldPassword!=null?param.oldPassword:''}"
    用于修改失败后保存之前的输入密码

8. 论坛功能开发

8.1 过滤敏感词

image-20230119151024259

使用前缀树来存储敏感词 :

  1. 根节点不包含字符,除根节点以外的每个节点,只包含一个字符

  2. 从根节点到某一个节点,路径上经过的字符连接起来,为该节点对应字符串

  3. 每个节点的所有子节点,包含的字符串不相同

核心 :

  1. 有一个指针1指向前缀树,用以遍历敏感词的每一个字符

  2. 有一个指针2指向被过滤字符串,用以标识敏感词的开头

  3. 有一个指针3指向被过滤字符串,用以标识敏感词的结尾

image-20230119152023464

过滤算法实现

在resources创建sensitive-words.txt文敏感词文本

package com.qiuyu.utils;

/**
 * 敏感词过滤器
 */

@Component
public class SensitiveFilter {
    private static final Logger logger = LoggerFactory.getLogger(SensitiveFilter.class);

    // 替换符
    private static final String REPLACEMENT = "***";

    // 根节点
    private TrieNode rootNode = new TrieNode();

    // 构造器之后运行
    @PostConstruct
    public void init() {
        try (
                // 读取文件流 BufferedReader带缓冲区效率更高
                InputStream is = this.getClass().getClassLoader().getResourceAsStream("sensitive-words.txt");
                BufferedReader reader = new BufferedReader(new InputStreamReader(is));
        ) {
            String keyword;
            // 一行一行读取文件中的字符
            while ((keyword = reader.readLine()) != null) {
                // 添加到前缀树
                this.addKeyword(keyword);
            }
        } catch (IOException e) {
            logger.error("加载敏感词文件失败: " + e.getMessage());
        }
    }

    /**
     * 将一个敏感词添加到前缀树中
     * 类似于空二叉树的插入
     */
    private void addKeyword(String keyword) {
        TrieNode tempNode = rootNode;
        for (int i = 0; i < keyword.length(); i++) {
            //将汉字转化为Char值
            char c = keyword.charAt(i);

            //找下有没有这个子节点,没有的话加入
            TrieNode subNode = tempNode.getSubNode(c);
            if (subNode == null) {
                // 初始化子节点并加入到前缀树中
                subNode = new TrieNode();
                tempNode.addSubNode(c, subNode);
            }

            // 指向子节点,进入下一轮循环
            tempNode = subNode;

            // 设置结束标识
            if (i == keyword.length() - 1) {
                tempNode.setKeywordEnd(true);
            }
        }
    }


    /**
     * 过滤敏感词
     * @param text 待过滤的文本
     * @return 过滤后的文本
     */
    public String filter(String text) {
        if (StringUtils.isBlank(text)) {
            return null;
        }

        // 指针1
        TrieNode tempNode = rootNode;
        // 指针2
        int begin = 0;
        // 指针3
        int position = 0;
        // 结果(StringBuilder:可变长度的String类)
        StringBuilder sb = new StringBuilder();

        //用position做结尾判断比begin指针少几次循环
        while (position < text.length()) {
            char c = text.charAt(position);

            // 跳过符号,比如 ☆赌☆博☆
            if (isSymbol(c)) {
                // 若指针1处于根节点,将此符号计入结果,让指针2向下走一步(就是不理他)
                if (tempNode == rootNode) {
                    sb.append(c);
                    begin++;
                }
                // 无论符号在开头或中间,指针3都向下走一步
                position++;
                continue;
            }

            // 检查下级节点
            tempNode = tempNode.getSubNode(c);
            if (tempNode == null) {
                // 以begin开头的字符串不是敏感词,直接加入结果
                sb.append(text.charAt(begin));
                // 进入下一个位置
                position = ++begin;
                // 重新指向根节点
                tempNode = rootNode;
            } else if (tempNode.isKeywordEnd()) {
                // 发现敏感词,将begin~position字符串替换掉
                sb.append(REPLACEMENT);
                // 进入下一个位置
                begin = ++position;
                // 重新指向根节点
                tempNode = rootNode;
            } else {
                // 检查下一个字符
                position++;
            }
        }

        // 将最后一批字符计入结果
        sb.append(text.substring(begin));

        return sb.toString();
    }

    // 判断是否为符号
    private boolean isSymbol(Character c) {
        // isAsciiAlphanumeric判断是否为字母或数字
        // 0x2E80~0x9FFF 是东亚文字范围
        return !CharUtils.isAsciiAlphanumeric(c) && (c < 0x2E80 || c > 0x9FFF);
    }


    // 内部类构造前缀树数据结构
    private class TrieNode {

        // 关键词结束标识
        private boolean isKeywordEnd = false;

        // 子节点(key是下级字符,value是下级节点)
        private Map<Character, TrieNode> subNodes = new HashMap<>();

        public boolean isKeywordEnd() {
            return isKeywordEnd;
        }

        public void setKeywordEnd(boolean keywordEnd) {
            isKeywordEnd = keywordEnd;
        }

        // 添加子节点
        public void addSubNode(Character c, TrieNode node) {
            subNodes.put(c, node);
        }

        // 获取子节点
        public TrieNode getSubNode(Character c) {
            return subNodes.get(c);
        }

    }
}

8.2 发布帖子

核心 :ajax异步:整个网页不刷新,访问服务器资源返回结果,实现局部的刷新。

实质:JavaScript和XML(但目前JSON的使用比XML更加普遍)

封装Fastjson工具类

导入FastJson

<dependency>
   <groupId>com.alibaba</groupId>
   <artifactId>fastjson</artifactId>
   <version>1.2.76</version>
</dependency>
/**
 * 使用fastjson,将JSON对象转为JSON字符串(前提要引入Fastjson)
 * @param code
 * @param msg
 * @param map
 * @return
 */
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) {
        //从map里的key集合中取出每一个key
        for (String key : map.keySet()) {
            json.put(key, map.get(key));
        }
    }
    return json.toJSONString();
}
public static String getJSONString(int code, String msg) {
    return getJSONString(code, msg, null);
}
public static String getJSONString(int code) {
    return getJSONString(code, null, null);
}

ajax请求demo

<input type="button" value="发送" onclick="send();">
    
//异步JS 
function send() {
    $.post(
        "/community/test/ajax",
        {"name":"张三","age":25},
        //回调函数返回结果
        function(data) {
            console.log(typeof (data));
            console.log(data);
            
            //json字符串转js对象
            data = $.parseJSON(data);
            
            console.log(typeof (data));
            console.log(data.code);
            console.log(data.msg);
        }
    )
}
/**
* Ajax异步请求示例
*/
@RequestMapping(value = "/ajax", method = RequestMethod.POST)
@ResponseBody
public String testAjax(String name, int age) {
    System.out.println(name);
    System.out.println(age);
    return CommunityUtil.getJSONString(200,"操作成功!");
}

Service层

/**
* 新增一条帖子
* @param post 帖子
* @return
*/
public int addDiscussPost(DiscussPost post){
    if(post == null){
        //不用map直接抛异常
        throw new IllegalArgumentException("参数不能为空!");
    }

    //转义< >等HTML标签为 &lt; &gt 让浏览器认为是普通字符,防止被注入
    post.setTitle(HtmlUtils.htmlEscape(post.getTitle()));
    post.setContent(HtmlUtils.htmlEscape(post.getContent()));

    //过滤敏感词
    post.setTitle(sensitiveFilter.filter(post.getTitle()));
    post.setContent(sensitiveFilter.filter(post.getContent()));

    return discussPostMapper.insert(post);
}

Controller层

/**
     *  添加帖子
     * @param title 标题
     * @param content 内容
     * @return
     */
@PostMapping("/add")
@ResponseBody
// @LoginRequired
public String addDiscussPost(String title, String content){
    //获取当前登录的用户
    User user = hostHolder.getUser();
    if (user == null){
        //403权限不够
        return CommunityUtil.getJSONString(403,"你还没有登录哦!");
    }
    if(StringUtils.isBlank(title) || StringUtils.isBlank(content)){
        return CommunityUtil.getJSONString(222,"贴子标题或内容不能为空!");
    }

    DiscussPost post = new DiscussPost();
    post.setUserId(user.getId().toString());
    post.setTitle(title);
    post.setContent(content);
    post.setType(0);
    post.setStatus(0);
    post.setCreateTime(new Date());

    //业务处理,将用户给的title,content进行处理并添加进数据库
    discussPostService.addDiscussPost(post);

    //返回Json格式字符串给前端JS,报错的情况将来统一处理
    return CommunityUtil.getJSONString(0,"发布成功!");
}

前端

注意:$.parseJSON(data) →通过jQuery,将服务端返回的JSON格式的字符串转为js对象

$(function(){
	$("#publishBtn").click(publish);
});

function publish() {
	$("#publishModal").modal("hide");
	/**
	 * 服务器处理
	 */
	// 获取标题和内容
	var title = $("#recipient-name").val();
	var content = $("#message-text").val();
	// 发送异步请求(POST)
	$.post(
		CONTEXT_PATH + "/discuss/add",
		//与Controller层两个属性要一致!!!
		{"title":title,"content":content},
		function(data) {
			//把json字符串转化成Js对象,后面才可以调用data.msg
			data = $.parseJSON(data);
			// 在提示框中显示返回消息
			$("#hintBody").text(data.msg);
			// 显示提示框
			$("#hintModal").modal("show");
			// 2秒后,自动隐藏提示框
			setTimeout(function(){
				$("#hintModal").modal("hide");
				// 成功,刷新页面
				if(data.code == 0) {
					window.location.reload();
				}
			}, 2000);
		}
	);
}

8.3 查看贴子详情

image-20230119174529922

Service

/**
 * 通过id查找帖子
 * @param id
 * @return
 */
public DiscussPost findDiscussPostById(int id){
    return discussPostMapper.selectById(id);
}

Controller

/**
     * 查看帖子详细页
     * @param discussPostId
     * @param model
     * @return
     */
@GetMapping( "/detail/{discussPostId}")
public String getDiscussPost(@PathVariable("discussPostId") int discussPostId, Model model){
    //通过前端传来的Id查询帖子
    DiscussPost post = discussPostService.findDiscussPostById(discussPostId);
    model.addAttribute("post",post);

    //用以显示发帖人的头像及用户名
    User user = userService.findUserById(post.getUserId());
    model.addAttribute("user",user);
    return "/site/discuss-detail";
}
  • 这里查询了两次,后面使用redis优化

前端

<a th:href="@{|/discuss/detail/${map.post.id}|}" th:utext="${map.post.title}">标题</a>
  • 如果在@{ }中想要常量和变量的拼接需要用两个| |
<b th:text="${#dates.format(post.createTime,'yyyy-MM-dd HH:mm:ss')}">时间</b>
  • #dates.format()用于格式化时间

8.4 事务管理

概念

1. 事务特性

image-20230119222706666

2. 事务的隔离性

image-20230119223006755

3.并发异常

  • 第一类丢失更新

  • 第二类丢失更新

  • 脏读

  • 不可重复读

  • 幻读

  • 事务1的回滚导致事务2更新的数据丢失了

img
  • 事务1的提交导致事务2更新的数据丢失了
img img img img img

img

Spring声明式事务

方法:

**1.通过XML配置 **

2.通过注解@Transaction,如下:

/* REQUIRED: 支持当前事务(外部事务),如果不存在则创建新事务
 * REQUIRED_NEW: 创建一个新事务,并且暂停当前事务(外部事务)
 * NESTED: 如果当前存在事务(外部事务),则嵌套在该事务中执行(独立的提交和回滚),否则就会和REQUIRED一样
 * 遇到错误,Sql回滚  (A->B)
 */
@Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED)

  • propagation用于配置事务传播机制,既两个带事务的方法AB,方法A调用方法B,事务以哪个为准

  • REQUIRED 外部事务为准,比如A调用B,以A为准,如果A有事务就按照A的事务来,如果A没有事务就创建一个新的事务

  • REQUIRED_NEW 创建一个新的事务,比如A调用B,直接无视(暂停)A的事务,B自己创建一个新的事务

  • NESTED 嵌套,如果当前存在事务(外部事务),则嵌套在该事务中执行(独立的提交和回滚),否则自己创建新事务

Spring编程式事务

控制粒度更低,比如一个方法要访问10次数据库,只有5次需要保证事务,就可以用编程式来控制,声明式会10次全都放入事务中

方法: 通过TransactionTemplate组件执行SQL管理事务,如下:

  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("Marry");
              user.setSalt(CommunityUtil.generateUUID().substring(0,5));
              user.setPassword(CommunityUtil.md5("123123")+user.getSalt());
              user.setType(0);
              user.setHeaderUrl("http://localhost:8080/2.png");
              user.setCreateTime(new Date());
              userMapper.insertUser(user);
              //设置error,验证事务回滚
              Integer.valueOf("abc");
              return "ok"; }
      });
 }

8.5 显示评论

Service层

package com.qiuyu.service;

@Service
public class CommentService {
    @Autowired
    private CommentMapper commentMapper;

    /**
     * 分页获得指定帖子的评论
     * @param entityType
     * @param entityId
     * @param page
     * @return
     */
    public IPage<Comment> findCommentsByEntity(int entityType, int entityId, IPage<Comment> page) {
        LambdaQueryWrapper<Comment> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(Comment::getEntityType, entityType).eq(Comment::getEntityId, entityId);
        commentMapper.selectPage(page,wrapper);
        return page;
    }

    /**
     * 获取某个帖子评论的数量
     * @param entityType
     * @param entityId
     * @return
     */
    public int findCommentCount(int entityType, int entityId){
        Integer count = commentMapper.selectCount(new LambdaQueryWrapper<Comment>()
                .eq(Comment::getEntityType, entityType)
                .eq(Comment::getEntityId, entityId));
        return count;
    }

}

Controller层

/**
     * 查看帖子详细页
     * @param discussPostId
     * @param model
     * @return
     */
@GetMapping( "/detail/{discussPostId}")
public String getDiscussPost(@PathVariable("discussPostId") int discussPostId, Model model, MyPage<Comment> page){
    //通过前端传来的Id查询帖子
    DiscussPost post = discussPostService.findDiscussPostById(discussPostId);
    model.addAttribute("post",post);

    //用以显示发帖人的头像及用户名
    User user = userService.findUserById(post.getUserId());
    model.addAttribute("user",user);

    //得到帖子的评论
    page.setSize(5);
    page.setPath("/discuss/detail/"+discussPostId);
    page = (MyPage<Comment>) commentService.findCommentsByEntity(ENTITY_TYPE_POST, post.getId(), page);
    //评论列表
    List<Comment> commentList = page.getRecords();

    // 评论: 给帖子的评论
    // 回复: 给评论的评论
    // 评论VO(viewObject)列表 (将comment,user信息封装到每一个Map,每一个Map再封装到一个List中)
    List<Map<String,Object>> commentVoList = new ArrayList<>();
    if(commentList != null){
        for (Comment comment : commentList) {
            //一条评论的VO
            Map<String, Object> commentVo = new HashMap<>(10);
            //评论
            commentVo.put("comment",comment);
            //评论作者
            commentVo.put("user",userService.findUserById(comment.getUserId().toString()));

            //回复
            Page<Comment> replyPage = new Page<>();
            replyPage.setCurrent(1);
            replyPage.setSize(Integer.MAX_VALUE);
            replyPage = (Page<Comment>) commentService.findCommentsByEntity(ENTITY_TYPE_COMMENT, comment.getId(), replyPage);
            //回复列表
            List<Comment> replyList = replyPage.getRecords();
            //回复的VO列表
            List<Map<String,Object>> replyVoList = new ArrayList<>();
            if(replyList != null){
                for (Comment reply : replyList) {
                    //一条回复的VO
                    Map<String, Object> replyVo = new HashMap<>(10);
                    //回复
                    replyVo.put("reply",reply);
                    //作者
                    replyVo.put("user",user);
                    //回复的目标
                    User target = reply.getTargetId() == 0 ? null : userService.findUserById(reply.getTargetId().toString());
                    replyVo.put("target",target);

                    replyVoList.add(replyVo);
                }
            }

            //回复列表放入评论
            commentVo.put("reply",replyVoList);

            //评论的回复数量
            int replyCount = commentService.findCommentCount(ENTITY_TYPE_COMMENT, comment.getId());
            commentVo.put("replyCount",replyCount);


            commentVoList.add(commentVo);
        }
    }

    model.addAttribute("comments",commentVoList);
    model.addAttribute("page",page);

    return "/site/discuss-detail";
}

前端

<li class="media pb-3 pt-3 mb-3 border-bottom" th:each="commentvo:${comments}">
  • th:each 循环
<span th:text="${(page.current-1) * page.size + commentvoStat.count}">1</span>#
  • commentvoStat.count 循环中默认带一个循环属性名+Stat的对象,使用count可以得到目前循环到第几个
<a th:href="|#huifu-${replyvoStat.count}|" data-toggle="collapse" >回复</a>

<div th:id="|huifu-${replyvoStat.count}|" class="mt-4 collapse"></div>
  • 这俩进行了一个id绑定

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/173545.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

关于JVM

作者&#xff1a;~小明学编程 文章专栏&#xff1a;JavaEE 格言&#xff1a;热爱编程的&#xff0c;终将被编程所厚爱。 目录 内存区域的划分 程序计数器&#xff08;线程私有&#xff09; Java虚拟机栈 (线程私有) 本地方法栈 堆&#xff08;线程共享&#xff09; 方法…

Leetcode:77. 组合、216. 组合总和 III(C++)

目录​​​​​​​ 77. 组合&#xff1a; 问题描述&#xff1a; 实现代码与解析&#xff1a; 递归&#xff08;回溯&#xff09;&#xff1a; 原理思路&#xff1a; 剪枝优化版&#xff1a; 原理思路&#xff1a; 216. 组合总和 III&#xff1a; 问题描述&#xff1a…

[C/C++]指针,指针数组,数组指针,函数指针

文章目录指针内存空间的访问方式指针变量的声明指针的赋值指针运算用指针处理数组元素指针数组用指针作为函数的参数指针型函数指向函数的指针指针 指针是C从C中继承过来的重要数据类型。通过指针技术可以描述各种复杂的数据结构&#xff0c;可以更加灵活的处理字符串&#xf…

Linux下dmi信息分析工具dmidecode原理

dmidecode命令主要是通过DMI获取主机的硬件信息&#xff0c;其输出的信息包括BIOS、系统、主板、处理器、内存、缓存等等。它是通过SMBIOS&#xff08;System Management BIOS)来获取信息的。SMBIOS是主板或系统制造者以标准格式显示产品管理信息所需遵循的统一规范。 什么是DM…

QA特辑 | 以万变钳制黑灰产之变的验证码产品设计逻辑的答案,都在这里

1月12 日下午&#xff0c;就验证码的攻防对抗问题&#xff0c;顶象反欺诈专家大卫从验证码的破解手段讲起&#xff0c;从防御角度深度剖析如何应对黑灰产的攻击以及验证码在产品能力设计层面应该考虑哪些问题。 直播也吸引了众多关注验证码的观众前来围观&#xff0c;针对验证…

vue3性能优化

文章目录1. Lighthouse1.1 性能参数2. rollup-plugin-visualizer&#xff08;打包代码块分析&#xff09;3. vite配置优化4. PWA离线缓存技术5. 其他优化1. Lighthouse 谷歌浏览器自带的 DevTools 也可以全局安装Lighthouse # 安装 yarn global add lighthouse# 使用 lighth…

Android app集成微信支付

Android app集成微信支付 鉴于微信支付的文档入口不太容易找到、以及文档中有些逻辑不通或者容易产生歧义或者缺失一些信息的情况&#xff0c;记录下此次接入的流程和需要关注的一些点。 使用的是app支付-> APP支付产品介绍 首先阅读介绍等&#xff0c;了解一些基础的概念…

c++数据结构-图(详解附算法代码,一看就懂)

图&#xff08;Graph&#xff09;是一种复杂的非线性结构&#xff0c;它可以描述数据间的关系&#xff0c;被广泛使用。图 G 由两个集合 V 和 E 组成&#xff0c;记为 。V是顶点的有穷非空集合&#xff0c;E是边的集合。通常&#xff0c;也将 G 的顶点集和边集表示为 V(G) 和 E…

尚医通-登录注册搭建-JWT(二十八)

目录&#xff1a; &#xff08;1&#xff09;前台用户系统-登录注册-需求分析 &#xff08;2&#xff09;前台用户系统-登录注册-搭建环境 &#xff08;3&#xff09;前台用户系统-手机登录-基本实现 &#xff08;4&#xff09;前台用户系统-手机登录-整合JWT &#xff08;…

【JUC并发编程】使用多线程可能带来什么问题

【JUC并发编程】使用多线程可能带来什么问题? 文章目录【JUC并发编程】使用多线程可能带来什么问题?什么是多线程并发为什么会出现线程带来的安全性问题可见性问题原子性问题有序性问题活跃性问题性能问题引起线程切换的几种方式什么是多线程 多线程意味着你能够在同一个应用…

Linux的ZONE_DMA,ZONE_NORMAL,ZONE_HIGHMEM及分配页释放页函数的简单介绍

Linux的ZONE_DMA&#xff0c;ZONE_NORMAL,ZONE_HIGHMEM及分配页释放页函数的简单介绍简单介绍一下页&#xff1a;Linux 区&#xff1a;分配页系统调用释放页系统调用简单介绍一下页&#xff1a; 内核把物理页作为内存管理的基本单位。 尽管处理器的最小可寻址单位通常为字(甚至…

ZooKeeper-分布式锁实现

4.11)Zookeeper分布式锁-概念 •在我们进行单机应用开发&#xff0c;涉及并发同步的时候&#xff0c;我们往往采用synchronized或者Lock的方式来解决多线程间的代码同步问题&#xff0c;这时多线程的运行都是在同一个JVM之下&#xff0c;没有任何问题。 •但当我们的应用是分…

【JavaScript】实现简易购物车

&#x1f4bb;【JavaScript】实现简易购物车 &#x1f3e0;专栏&#xff1a;有趣实用案例 &#x1f440;个人主页&#xff1a;繁星学编程&#x1f341; &#x1f9d1;个人简介&#xff1a;一个不断提高自我的平凡人&#x1f680; &#x1f50a;分享方向&#xff1a;目前主攻前端…

客快物流大数据项目(一百零四):为什么选择Elastic Search作为存储服务

文章目录 为什么选择Elastic Search作为存储服务 一、​​​​​​​​​​​​​​ElasticSearch简介

【GD32F427开发板试用】懒人新手试用

本篇文章来自极术社区与兆易创新组织的GD32F427开发板评测活动&#xff0c;更多开发板试用活动请关注极术社区网站。作者&#xff1a;东东_dxGGN2 我收到的开发板是GD32F427R-START&#xff0c;MCU是GD32F427RKT6&#xff0c;如下图&#xff08;座机拍的见谅&#xff09; 测试流…

【C++】从0到1入门C++编程学习笔记 - 核心编程篇:内存分区模型

文章目录一、程序运行前二、程序运行后三、new 操作符C程序在执行时&#xff0c;将内存大方向划分为4个区域 代码区&#xff1a;存放函数体的二进制代码&#xff0c;由操作系统进行管理的全局区&#xff1a;存放全局变量和静态变量以及常量栈区&#xff1a;由编译器自动分配释…

2022年回顾 | 被磨砺,被厚待

岁末年首&#xff0c; 最宜盘点过往的时光。 回顾2022团结一心&#xff0c;攻坚克难&#xff0c; 祝福2023大展宏图&#xff0c;鹏程万里。 2022我们遇到了"卷土重来"、 “挥之不去”&#xff0c; 也等到了"再也不见"和 “永远下线”。 2022是一个&…

HTML中的table标签与a标签

这里写自定义目录标题一、table标签1、什么是table标签2、table标签中长见到的标签3、例子代码及其结果二、a标签1、什么是a标签2、a标签中常见的属性3、例子代码及其结果一、table标签 1、什么是table标签 table标签表示整体的一个表格 2、table标签中长见到的标签 <tr…

基于Spring Boot和Spring Cloud实现微服务架构

首先&#xff0c;最想说的是&#xff0c;当你要学习一套最新的技术时&#xff0c;官网的英文文档是学习的最佳渠道。因为网上流传的多数资料是官网翻译而来&#xff0c;很多描述的重点也都偏向于作者自身碰到的问题&#xff0c;这样就很容易让你理解和操作出现偏差&#xff0c;…

采用特殊硬件指令对密码学算法加速

1. 引言 Armando Faz-Hermandez等人2018年论文《SoK: A Performance Evaluation of Cryptographic Instruction Sets on Modern Architectures》&#xff0c;开源代码见&#xff1a; https://github.com/armfazh/flo-shani-aesni&#xff08;C语言&#xff09; slide见&…