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>>
的用法。这是因为:
- 灵活性: 使用
Map<String, Object>
能够提供更大的灵活性。当你不确定查询结果的具体结构,或者需要返回多个不同类型的字段时,Map
可以方便地存储这些异构数据。这在处理复杂的SQL查询或联表查询时尤其有用,因为查询结果可能包含多个表的字段,而这些字段可能没有直接映射到单一的实体类。 - 动态查询: 当你需要进行动态SQL查询时,使用
Map
可以更方便地构造和解析查询结果。例如,你可能需要根据不同的条件组合查询不同的字段,这时Map
提供了一种灵活的方式来处理这种情况。 - 避免实体类映射问题: 如果你的查询结果包含了额外的计算字段或聚合函数结果,这些字段可能无法直接映射到已有的实体类属性上。使用
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 就可以了
乐观锁
乐观锁的实现通常包括以下步骤:
- 读取记录时,获取当前的版本号(version)。
- 在更新记录时,将这个版本号一同传递。
- 执行更新操作时,设置
version = newVersion
的条件为version = oldVersion
。 - 如果版本号不匹配,则更新失败
/**
* 根据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);
}