SpringBoot-微头条项目实战

SpringBoot-微头条项目实战

application.yaml文件

spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    # JDBC四大件
    url: jdbc:mysql://localhost:3306/mybatis-example
    username: root
    password: Zhuwenxue2002
    driver-class-name: com.mysql.cj.jdbc.Driver

mybatis-plus:
  type-aliases-package: com.xiaobai.pojo # 给实体类起别名
  global-config:
    db-config:
      logic-delete-field: deleted # 为所有表配置逻辑删除属性
      id-type: auto # 配置主键由MySql自增长
      table-prefix: news_ # 配置表前缀为news_

pom.xml maven依赖导入文件

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.3.1</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.xiaobai</groupId>
    <artifactId>spring-headline</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>spring-headline</name>
    <description>spring-headline</description>
    <url/>
    <licenses>
        <license/>
    </licenses>
    <developers>
        <developer/>
    </developers>
    <scm>
        <connection/>
        <developerConnection/>
        <tag/>
        <url/>
    </scm>
    <properties>
        <java.version>17</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- https://mvnrepository.com/artifact/com.baomidou/mybatis-plus-spring-boot3-starter -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
            <version>3.5.7</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/com.alibaba/druid -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.2.23</version>
        </dependency>

        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

配置SpringBoot启动类

为MyBatisPlus添加插件

@SpringBootApplication
public class SpringHeadlineApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringHeadlineApplication.class, args);
    }

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); // 添加分页插件,配置数据库类型为MySQL
        interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); // 添加乐观锁插件
        interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor()); // 防止全表更新或删除插件
        return interceptor;
    }
    
}

工具类

结果封装类
/**
 * 全局统一返回结果类
 */
public class Result<T> {
    // 返回码
    private Integer code;
    // 返回消息
    private String message;
    // 返回数据
    private T data;
    public Result(){}
    // 返回数据
    protected static <T> Result<T> build(T data) {
        Result<T> result = new Result<T>();
        if (data != null)
            result.setData(data);
        return result;
    }
    public static <T> Result<T> build(T body, Integer code, String message) {
        Result<T> result = build(body);
        result.setCode(code);
        result.setMessage(message);
        return result;
    }
    public static <T> Result<T> build(T body, ResultCodeEnum resultCodeEnum) {
        Result<T> result = build(body);
        result.setCode(resultCodeEnum.getCode());
        result.setMessage(resultCodeEnum.getMessage());
        return result;
    }
    /**
     * 操作成功
     * @param data  baseCategory1List
     * @param <T>
     * @return
     */
    public static<T> Result<T> ok(T data){
        Result<T> result = build(data);
        return build(data, ResultCodeEnum.SUCCESS);
    }
    public Result<T> message(String msg){
        this.setMessage(msg);
        return this;
    }
    public Result<T> code(Integer code){
        this.setCode(code);
        return this;
    }
    public Integer getCode() {
        return code;
    }
    public void setCode(Integer code) {
        this.code = code;
    }
    public String getMessage() {
        return message;
    }
    public void setMessage(String message) {
        this.message = message;
    }
    public T getData() {
        return data;
    }
    public void setData(T data) {
        this.data = data;
    }
}

结果枚举类
/**
 * 统一返回结果状态信息类
 *
 */
public enum ResultCodeEnum {

    SUCCESS(200,"success"),
    USERNAME_ERROR(501,"usernameError"),
    PASSWORD_ERROR(503,"passwordError"),
    NOTLOGIN(504,"notLogin"),
    USERNAME_USED(505,"userNameUsed");

    private Integer code;
    private String message;
    private ResultCodeEnum(Integer code, String message) {
        this.code = code;
        this.message = message;
    }
    public Integer getCode() {
        return code;
    }
    public String getMessage() {
        return message;
    }
}


MD5加密工具类
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

public final class MD5Util {
    public static String encrypt(String strSrc) {
        try {
            char hexChars[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8',
                    '9', 'a', 'b', 'c', 'd', 'e', 'f' };
            byte[] bytes = strSrc.getBytes();
            MessageDigest md = MessageDigest.getInstance("MD5");
            md.update(bytes);
            bytes = md.digest();
            int j = bytes.length;
            char[] chars = new char[j * 2];
            int k = 0;
            for (int i = 0; i < bytes.length; i++) {
                byte b = bytes[i];
                chars[k++] = hexChars[b >>> 4 & 0xf];
                chars[k++] = hexChars[b & 0xf];
            }
            return new String(chars);
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
            throw new RuntimeException("MD5加密出错!!+" + e);
        }
    }
}

Token

Token是一项技术,可以理解为接口

JWT(Json Web Token)时具体生成校验,解析等动作,可以理解为Token的实现类

<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>${jjwt.version}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/javax.xml.bind/jaxb-api -->
<dependency>
    <groupId>javax.xml.bind</groupId>
    <artifactId>jaxb-api</artifactId>
    <version>${jaxb.version}</version>
</dependency>

application.yaml
jwt:
  token:
    tokenExpiration: 120 # Token有效时间,单位(分钟)
    tokenSignKey: headline123456 # 当前程序签名密钥,自定义

Token工具类

注:这个工具类由尚硅谷提供,支持JJWT版本为0.9.1,必须导入2.3.1版本的jaxb才可正常使用

package com.xiaobai.util;

import com.alibaba.druid.util.StringUtils;
import io.jsonwebtoken.*;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import java.util.Date;

@Data
@Component
@ConfigurationProperties(prefix = "jwt.token")
public class JwtHelper {

    private long tokenExpiration; //有效时间,单位毫秒 1000毫秒 == 1秒
    private String tokenSignKey;  //当前程序签名秘钥

    //生成token字符串
    public String createToken(Long userId) {
        System.out.println("tokenExpiration = " + tokenExpiration);
        System.out.println("tokenSignKey = " + tokenSignKey);
        return Jwts.builder()

                .setSubject("YYGH-USER")
                .setExpiration(new Date(System.currentTimeMillis() + tokenExpiration * 1000 * 60)) //单位分钟
                .claim("userId", userId)
                .signWith(SignatureAlgorithm.HS512, tokenSignKey)
                .compressWith(CompressionCodecs.GZIP)
                .compact();
    }

    //从token字符串获取userid
    public Long getUserId(String token) {
        if (StringUtils.isEmpty(token)) return null;
        Jws<Claims> claimsJws = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token);
        Claims claims = claimsJws.getBody();
        Integer userId = (Integer) claims.get("userId");
        return userId.longValue();
    }


    //判断token是否有效
    public boolean isExpiration(String token) {
        try {
            //没有过期,有效,返回false
            return Jwts.parser()
                    .setSigningKey(tokenSignKey)
                    .parseClaimsJws(token)
                    .getBody()
                    .getExpiration().before(new Date());
        } catch (Exception e) {
            //过期出现异常,返回true
            return true;
        }
    }
}

拦截器检查Token

在用户登录之后,就有权限对数据进行增删改操作了

而我们需要在每次进行这种操作之前都要检查他的Token是否过期,是否仍然处于登陆状态

这种动作时重复冗余的,我们使用拦截器将其提取出来

package com.xiaobai.interceptor;

/**
 * 登录保护拦截器,在对数据进行操作前检测token是否有效
 */
@Component
public class LoginProtectedInterceptor implements HandlerInterceptor {
    @Autowired
    private JwtHelper jwtHelper;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String token = request.getHeader("token");

        boolean expiration = jwtHelper.isExpiration(token);
        if (expiration) {
            // 通过手动封装一个Json,再通过response写回
            Result<Object> result = Result.build(null, ResultCodeEnum.NOTLOGIN);
            ObjectMapper objectMapper = new ObjectMapper();
            String json = objectMapper.writeValueAsString(result);
            response.getWriter().write(json);
            return false; // 拦截
        }
        return true; // 放行
    }
}

最后我们需要创建MVC配置类,配置开启拦截器及其生效路径

package com.xiaobai.config;

@Configuration
public class WebMVCConfig implements WebMvcConfigurer {
    @Autowired
    private LoginProtectedInterceptor  loginProtectedInterceptor;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginProtectedInterceptor).addPathPatterns("/headline/**");
    }
}

遇到的错误

org.apache.ibatis.binding.BindingException: Invalid bound statement (not found): com.xiaobai.mapper.UserMapper.selectById

在使用BaseMapper提供的方法时,遇到报错

使用@TableId注解标识主键,给MyBatisPlus底层指向主键位置,ById才会找到主键


IPage的Map泛型

IPage的泛型一般为实体类类型,但我们要求返回的数据类型为键值对的map类型

IPage的泛型怎么可以使用Map类型呢? 这令我十分震惊。

但万能的神奇海螺还是给我了最终的解释:

在 MyBatis Plus 中,IPage<T> 接口是一个泛型接口,通常被用来处理分页查询的结果。T 是这个接口的泛型参数,它代表了分页数据中的元素类型。当我们在使用 IPage 进行分页查询时,T 通常会被设置为具体的实体类类型,这样我们就能直接获取到符合实体模型的数据。

然而,在某些情况下,你可能会看到 IPage<Map<String, Object>> 的用法。这是因为:

  1. 灵活性: 使用 Map<String, Object> 能够提供更大的灵活性。当你不确定查询结果的具体结构,或者需要返回多个不同类型的字段时,Map 可以方便地存储这些异构数据。这在处理复杂的SQL查询或联表查询时尤其有用,因为查询结果可能包含多个表的字段,而这些字段可能没有直接映射到单一的实体类。
  2. 动态查询: 当你需要进行动态SQL查询时,使用 Map 可以更方便地构造和解析查询结果。例如,你可能需要根据不同的条件组合查询不同的字段,这时 Map 提供了一种灵活的方式来处理这种情况。
  3. 避免实体类映射问题: 如果你的查询结果包含了额外的计算字段或聚合函数结果,这些字段可能无法直接映射到已有的实体类属性上。使用 Map 可以避免这种映射上的困难。

然而,使用 Map 也有其缺点:

  • 类型安全: 使用 Map 会牺牲类型安全性,因为你不再能够从编译器那里获得类型检查的好处。这意味着在运行时,你必须手动进行类型转换,这可能会引入潜在的 ClassCastException 错误。
  • 可读性和维护性: 使用实体类通常会让代码更加清晰和易于维护,因为实体类封装了业务逻辑和属性,而 Map 则可能需要额外的注释来解释字段的意义。

因此,虽然 IPage<Map<String, Object>> 可以在某些场景下提供灵活性,但在大多数情况下,使用具体的实体类作为泛型参数将更加推荐,因为它提供了更好的类型安全性和代码可读性。如果需要复杂查询结果的灵活性,你可以考虑创建一个专门的 DTO(Data Transfer Object)类来承载这些数据,这样既能保持类型安全,又能提高代码的可读性和可维护性。

csdn告诉我的!!!

  MyBatis 查询 MySQL 数据库,返回结果可以是具体的类、Map、List<Map> 等等。将查询结果返回 Map 类型的优点是,不需要为本次查询额外创建类。如果只查询某几个特定的列,且不想额外创建类的话,就可以将结果返回 Map 或 List<Map>
如果能明确查询结果只有一条记录时,返回 Map;如果查询结果可能有多条记录,返回 List<Map>
实现方式较为简单,只要在 DAO 层 XML 文件中,设定 resultType 而不设定 resultMap 就可以了


乐观锁

乐观锁的实现通常包括以下步骤:

  1. 读取记录时,获取当前的版本号(version)。
  2. 在更新记录时,将这个版本号一同传递。
  3. 执行更新操作时,设置 version = newVersion 的条件为 version = oldVersion
  4. 如果版本号不匹配,则更新失败
/**
 * 根据Hid查询头条详情
 * 1. 修改浏览量+1
 * 2. 多表查询
 * 注:因为乐观锁版本号问题,我们应该先查询,再修改
 *
 * @param hid 头条id
 * @return 多表属性Vo类
 */
@Override
public Result showHeadlineDetail(Integer hid) {
    // 查询到头条详情
    Map data = headlineMapper.queryDetailMap(hid);
    Map headlineMap = new HashMap();
    headlineMap.put("headline", data);

    // 修改浏览量+1
    Headline headline = new Headline();
    headline.setHid((Integer) data.get("hid"));
    // 将拿到的版本号设置为要update的版本号,如果不一致则更新失败
    headline.setVersion((Integer) data.get("version")); 
    headline.setPageViews((Integer) data.get("pageViews") + 1);
    headlineMapper.updateById(headline); // 修改执行语句

    return Result.ok(headlineMap);
}

IService

我们在之前的学习中,一直都是认为controller层不做业务处理,而将业务处理全部交给service层

这与IService的理念背道而驰,我们现在可以让controller直接调用service中IService提供的一些简单的增删改方法

直接在controller层将业务处理完毕,无需一直向下调用

@PostMapping("removeByHid") // 根据头条id删除该头条
public Result removeByHid(@RequestParam Integer hid) {
    headlineService.removeById(hid);
    return Result.ok(null);
}