SpringBoot-微头条项目实战
application.yaml文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| spring: datasource: type: com.alibaba.druid.pool.DruidDataSource 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 table-prefix: news_
|
pom.xml maven依赖导入文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92
| <?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/> </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> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-spring-boot3-starter</artifactId> <version>3.5.7</version> </dependency> <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添加插件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| @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)); interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor()); return interceptor; } }
|
工具类
结果封装类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
|
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; }
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; } }
|
结果枚举类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
|
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加密工具类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| 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的实现类
1 2 3 4 5 6 7 8 9 10 11 12
| <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>${jjwt.version}</version> </dependency>
<dependency> <groupId>javax.xml.bind</groupId> <artifactId>jaxb-api</artifactId> <version>${jaxb.version}</version> </dependency>
|
application.yaml
1 2 3 4
| jwt: token: tokenExpiration: 120 tokenSignKey: headline123456
|
Token工具类
注:这个工具类由尚硅谷提供,支持JJWT版本为0.9.1,必须导入2.3.1版本的jaxb才可正常使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
| 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; private String tokenSignKey;
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(); }
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(); }
public boolean isExpiration(String token) { try { return Jwts.parser() .setSigningKey(tokenSignKey) .parseClaimsJws(token) .getBody() .getExpiration().before(new Date()); } catch (Exception e) { return true; } } }
|
拦截器检查Token
在用户登录之后,就有权限对数据进行增删改操作了
而我们需要在每次进行这种操作之前都要检查他的Token是否过期,是否仍然处于登陆状态
这种动作时重复冗余的,我们使用拦截器将其提取出来
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| package com.xiaobai.interceptor;
@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) { 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配置类,配置开启拦截器及其生效路径
1 2 3 4 5 6 7 8 9 10 11
| 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。
- 如果版本号不匹配,则更新失败
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
|
@Override public Result showHeadlineDetail(Integer hid) { Map data = headlineMapper.queryDetailMap(hid); Map headlineMap = new HashMap(); headlineMap.put("headline", data);
Headline headline = new Headline(); headline.setHid((Integer) data.get("hid")); 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层将业务处理完毕,无需一直向下调用
1 2 3 4 5
| @PostMapping("removeByHid") public Result removeByHid(@RequestParam Integer hid) { headlineService.removeById(hid); return Result.ok(null); }
|