SpringBoot
Spring Boot 中文文档 (springdoc.cn)
1 接口文档
2 环境搭建
- 执行资料中的
big_event.sql
脚本,准备数据库表。
-- 创建数据库
create database big_event;
-- 使用数据库
use big_event;
-- 用户表
create table user (
id int unsigned primary key auto_increment comment 'ID',
username varchar(20) not null unique comment '用户名',
password varchar(32) comment '密码',
nickname varchar(10) default '' comment '昵称',
email varchar(128) default '' comment '邮箱',
user_pic varchar(128) default '' comment '头像',
create_time datetime not null comment '创建时间',
update_time datetime not null comment '修改时间'
) comment '用户表';
-- 分类表
create table category(
id int unsigned primary key auto_increment comment 'ID',
category_name varchar(32) not null comment '分类名称',
category_alias varchar(32) not null comment '分类别名',
create_user int unsigned not null comment '创建人ID',
create_time datetime not null comment '创建时间',
update_time datetime not null comment '修改时间',
constraint fk_category_user foreign key (create_user) references user(id) -- 外键约束
);
......
- 创建
springboot工程
,引入对应的依赖(web、mybatis、mysql
驱动)
- 配置文件
application.yml
中引入mybatis
的配置信息
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/big_event
username: root
password: 123456
data:
redis:
host: localhost
port: 6379
mybatis:
configuration:
map-underscore-to-camel-case: true #开启驼峰命名和下划线命名的自动转换
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
server:
port: 8080
- 创建包结构,并准备实体类
3 用户相关接口实现
// UserController
@RestController
@RequestMapping("/user")
@Validated
public class UserController {
@Autowired
private UserService userService;
@Autowired
private StringRedisTemplate stringRedisTemplate;
......
}
// UserServiceImpl
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
......
}
// UserMapper
@Mapper
public interface UserMapper {
......
}
// pojo.User
//lombok 在编译阶段,为实体类自动生成setter getter toString
// pom 文件中引入依赖 在实体类上添加注解
@Data
public class User {
@NotNull
private Integer id;//主键ID
private String username;//用户名
@JsonIgnore//让springmvc把当前对象转换成json字符串的时候,忽略password,最终的json字符串中就没有password这个属性了
private String password;//密码
@NotEmpty
@Pattern(regexp = "^\\S{1,10}$")
private String nickname;//昵称
@NotEmpty
@Email
private String email;//邮箱
private String userPic;//用户头像地址
private LocalDateTime createTime;//创建时间
private LocalDateTime updateTime;//更新时间
}
lombok
在Spring Boot中,Lombok是一个非常方便的工具,可以通过简单的注解来简化Java开发中的样板代码,例如getter、setter、构造函数等。通过引入Lombok依赖,可以避免编写大量重复的样板代码,提高代码的可读性和可维护性。
要在Spring Boot项目中使用Lombok,首先需要在项目的pom.xml文件中添加Lombok的依赖:
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.22</version>
<scope>provided</scope>
</dependency>
接下来,在Java类中使用Lombok注解来简化代码。以下是几个常用的Lombok注解示例:
- @Data:在类上添加@Data注解,会自动生成getter、setter、equals、hashCode和toString等方法。
import lombok.Data;
@Data
public class User {
private Long id;
private String name;
}
- @NoArgsConstructor:生成无参构造函数。
import lombok.NoArgsConstructor;
@NoArgsConstructor
public class User {
private Long id;
private String name;
}
- @AllArgsConstructor:生成全参构造函数。
import lombok.AllArgsConstructor;
@AllArgsConstructor
public class User {
private Long id;
private String name;
}
- @Slf4j:简化日志打印。
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class MyService {
public void doSomething() {
log.info("Doing something...");
}
}
通过使用这些Lombok注解,可以减少Java类中样板代码的编写,提高开发效率。需要注意的是,在使用Lombok的注解时,IDE中需要安装Lombok插件才能正常显示生成的方法。安装Lombok插件后,就可以像普通类一样调用生成的方法,而不用编写这些常见的代码了。
3.1 注册
// UserController
@PostMapping("/register")
public Result register(@Pattern(regexp = "^\\S{5,16}$") String username, @Pattern(regexp = "^\\S{5,16}$") String password) {
//查询用户
User u = userService.findByUserName(username);
if (u == null) {
//没有占用
//注册
userService.register(username, password);
return Result.success();
} else {
//占用
return Result.error("用户名已被占用");
}
}
// UserServiceImpl
@Override
public User findByUserName(String username) {
User u = userMapper.findByUserName(username);
return u;
}
@Override
public void register(String username, String password) {
//加密
String md5String = Md5Util.getMD5String(password);
//添加
userMapper.add(username,md5String);
}
// UserMapper
//根据用户名查询用户
@Select("select * from user where username=#{username}")
User findByUserName(String username);
//添加
@Insert("insert into user(username,password,create_time,update_time)" +
" values(#{username},#{password},now(),now())")
void add(String username, String password);
//`UserMapper`使用了`MyBatis`的注解来映射数据库操作:
//
// 1. `@Select("select * from user where username=#{username}")`:
// - 这是`MyBatis`中的`@Select`注解,用于执行查询操作,括号中的内容为SQL查询语句。
// - `select * from user where username=#{username}`表示在数据库表`User`中根据用户名查询用户信息,其中`#{username}`是一个占位符,会在SQL执行时被具体的参数替代。
//
// 2. `@Insert("insert into user(username,password,create_time,update_time) values(#{username},#{password},now(),now())")`:
// - 这是`MyBatis`中的`@Insert`注解,用于执行插入操作,括号中的内容为SQL插入语句。
// - `insert into user(username,password,create_time,update_time) values(#{username},#{password},now(),now())`表示向数据库表"User"中插入用户信息,包括用户名、加密后的密码以及创建时间和更新时间。
// - `#{username}`和`#{password}`是占位符,会在SQL执行时被具体的参数替代。
// - `now()`表示当前时间,用于设置创建时间和更新时间为当前操作的时间。
- UserController:
@PostMapping("/register")
: 这是一个用于处理POST请求的方法,路径为/register
。public Result register(@Pattern(regexp = "^\\S{5,16}$") String username, @Pattern(regexp = "^\\S{5,16}$") String password)
: 这个方法接收两个参数,用户名和密码,分别使用正则表达式验证了输入的内容格式。- 首先通过
userService.findByUserName(username)
查询数据库中是否已存在相同用户名的用户。 - 如果用户不存在,则调用
userService.register(username, password)
方法进行用户注册,将明文密码加密后保存到数据库,并返回成功的结果。 - 如果用户已存在,则返回一个包含错误信息的Result对象。
- UserServiceImpl:
findByUserName(String username)
: 根据用户名查询用户,调用userMapper.findByUserName(username)
查询数据库并返回结果。register(String username, String password)
: 注册用户,对密码进行MD5加密后,调用userMapper.add(username, md5String)
将用户信息插入数据库。
- UserMapper:
findByUserName(String username)
: 根据用户名查询用户,使用SQL语句查询数据库中的用户信息。add(String username, String password)
: 添加用户,使用SQL语句将用户名、加密后的密码以及创建时间和更新时间插入到数据库中。
3.2 登录
// UserController
@PostMapping("/login")
public Result<String> login(@Pattern(regexp = "^\\S{5,16}$") String username, @Pattern(regexp = "^\\S{5,16}$") String password) {
//根据用户名查询用户
User loginUser = userService.findByUserName(username);
//判断该用户是否存在
if (loginUser == null) {
return Result.error("用户名错误");
}
//判断密码是否正确 loginUser对象中的password是密文
if (Md5Util.getMD5String(password).equals(loginUser.getPassword())) {
//登录成功
Map<String, Object> claims = new HashMap<>();
claims.put("id", loginUser.getId());
claims.put("username", loginUser.getUsername());
String token = JwtUtil.genToken(claims);
//把token存储到redis中
ValueOperations<String, String> operations = stringRedisTemplate.opsForValue();
operations.set(token,token,1, TimeUnit.HOURS);
return Result.success(token);
}
return Result.error("密码错误");
}
//
3.3 获取用户详细信息
// UserController
@GetMapping("/userInfo")
public Result<User> userInfo(/*@RequestHeader(name = "Authorization") String token*/) {
//根据用户名查询用户
Map<String, Object> map = ThreadLocalUtil.get();
String username = (String) map.get("username");
User user = userService.findByUserName(username);
return Result.success(user);
}
3.4 更新用户密码
// UserController
@PatchMapping("/updatePwd")
public Result updatePwd(@RequestBody Map<String, String> params,@RequestHeader("Authorization") String token) {
//1.校验参数
String oldPwd = params.get("old_pwd");
String newPwd = params.get("new_pwd");
String rePwd = params.get("re_pwd");
if (!StringUtils.hasLength(oldPwd) || !StringUtils.hasLength(newPwd) || !StringUtils.hasLength(rePwd)) {
return Result.error("缺少必要的参数");
}
//原密码是否正确
//调用userService根据用户名拿到原密码,再和old_pwd比对
Map<String,Object> map = ThreadLocalUtil.get();
String username = (String) map.get("username");
User loginUser = userService.findByUserName(username);
if (!loginUser.getPassword().equals(Md5Util.getMD5String(oldPwd))){
return Result.error("原密码填写不正确");
}
//newPwd和rePwd是否一样
if (!rePwd.equals(newPwd)){
return Result.error("两次填写的新密码不一样");
}
//2.调用service完成密码更新
userService.updatePwd(newPwd);
//删除redis中对应的token
ValueOperations<String, String> operations = stringRedisTemplate.opsForValue();
operations.getOperations().delete(token);
return Result.success();
}
// UserServiceImpl
@Override
public void updatePwd(String newPwd) {
Map<String,Object> map = ThreadLocalUtil.get();
Integer id = (Integer) map.get("id");
userMapper.updatePwd(Md5Util.getMD5String(newPwd),id);
}
// UserMapper
@Update("update user set password=#{md5String},update_time=now() where id=#{id}")
void updatePwd(String md5String, Integer id);
这段代码展示了一个用户密码更新的过程,包括控制器(UserController)、服务实现类(UserServiceImpl)和数据访问层(UserMapper):
UserController:
@PatchMapping("/updatePwd")
注解表示该方法用于处理 PATCH 请求,路径为 “/updatePwd”。updatePwd
方法接收一个包含参数和请求头的 Map 对象和一个名为 “Authorization” 的请求头,用于更新用户密码。- 首先校验参数,包括旧密码、新密码和确认密码是否存在。
- 从
ThreadLocalUtil
中获取用户信息(在拦截器中存储的业务数据),获取用户名,然后通过用户名查询数据库获取用户信息。 - 检查旧密码是否正确,如果不正确则返回错误信息。
- 检查新密码和确认密码是否一致,如果不一致则返回错误信息。
- 调用
userService.updatePwd(newPwd)
方法更新用户密码,然后从 Redis 中删除对应的令牌,最后返回成功的结果。
UserServiceImpl:
updatePwd
方法用于更新用户密码,接收一个新密码参数。- 从
ThreadLocalUtil
中获取用户信息,获取用户 ID。 - 调用
userMapper.updatePwd(Md5Util.getMD5String(newPwd), id)
方法更新用户密码,使用 MD5 加密存储密码。
UserMapper:
@Update("update user set password=#{md5String},update_time=now() where id=#{id}")
是 MyBatis 的注解,表示执行更新用户密码的 SQL 语句。updatePwd
方法用于更新用户密码,接收一个经过 MD5 加密的新密码和用户 ID,更新用户密码和更新时间。
4 工具类介绍
4.1 Result
package com.itheima.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
//统一响应结果
@NoArgsConstructor
@AllArgsConstructor
@Data
public class Result<T> {
private Integer code;//业务状态码 0-成功 1-失败
private String message;//提示信息
private T data;//响应数据
//快速返回操作成功响应结果(带响应数据)
public static <E> Result<E> success(E data) {
return new Result<>(0, "操作成功", data);
}
//快速返回操作成功响应结果
public static Result success() {
return new Result(0, "操作成功", null);
}
public static Result error(String message) {
return new Result(1, message, null);
}
}
在Spring Boot
中给post
请求返回json
可以通过@RestController
注解来实现。这个注解表示这个类中的方法会默认返回json格式的数据。
4.2 Md5Util
public class Md5Util {
/**
* 默认的密码字符串组合,用来将字节转换成 16 进制表示的字符,apache校验下载的文件的正确性用的就是默认的这个组合
*/
protected static char hexDigits[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
protected static MessageDigest messagedigest = null;
static {
try {
messagedigest = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException nsaex) {
System.err.println(Md5Util.class.getName() + "初始化失败,MessageDigest不支持MD5Util。");
nsaex.printStackTrace();
}
}
/**
* 生成字符串的md5校验值
*
* @param s
* @return
*/
public static String getMD5String(String s) {
return getMD5String(s.getBytes());
}
/**
* 判断字符串的md5校验码是否与一个已知的md5码相匹配
*
* @param password 要校验的字符串
* @param md5PwdStr 已知的md5校验码
* @return
*/
public static boolean checkPassword(String password, String md5PwdStr) {
String s = getMD5String(password);
return s.equals(md5PwdStr);
}
public static String getMD5String(byte[] bytes) {
messagedigest.update(bytes);
return bufferToHex(messagedigest.digest());
}
private static String bufferToHex(byte bytes[]) {
return bufferToHex(bytes, 0, bytes.length);
}
private static String bufferToHex(byte bytes[], int m, int n) {
StringBuffer stringbuffer = new StringBuffer(2 * n);
int k = m + n;
for (int l = m; l < k; l++) {
appendHexPair(bytes[l], stringbuffer);
}
return stringbuffer.toString();
}
private static void appendHexPair(byte bt, StringBuffer stringbuffer) {
char c0 = hexDigits[(bt & 0xf0) >> 4];// 取字节中高 4 位的数字转换, >>>
// 为逻辑右移,将符号位一起右移,此处未发现两种符号有何不同
char c1 = hexDigits[bt & 0xf];// 取字节中低 4 位的数字转换
stringbuffer.append(c0);
stringbuffer.append(c1);
}
}
4.3 JwtUtil
public class JwtUtil {
private static final String KEY = "itheima";
//接收业务数据,生成token并返回
public static String genToken(Map<String, Object> claims) {
return JWT.create()
.withClaim("claims", claims)
.withExpiresAt(new Date(System.currentTimeMillis() + 1000 * 60 * 60 ))
.sign(Algorithm.HMAC256(KEY));
}
//接收token,验证token,并返回业务数据
public static Map<String, Object> parseToken(String token) {
return JWT.require(Algorithm.HMAC256(KEY))
.build()
.verify(token)
.getClaim("claims")
.asMap();
}
}
4.4 ThreadLocalUtil
@SuppressWarnings("all")
public class ThreadLocalUtil {
//提供ThreadLocal对象,
private static final ThreadLocal THREAD_LOCAL = new ThreadLocal();
//根据键获取值
public static <T> T get(){
return (T) THREAD_LOCAL.get();
}
//存储键值对
public static void set(Object value){
THREAD_LOCAL.set(value);
}
//清除ThreadLocal 防止内存泄漏
public static void remove(){
THREAD_LOCAL.remove();
}
}
4.5 LoginInterceptor
interceptors.LoginInterceptor
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//令牌验证
String token = request.getHeader("Authorization");
//验证token
try {
//从redis中获取相同的token
ValueOperations<String, String> operations = stringRedisTemplate.opsForValue();
String redisToken = operations.get(token);
if (redisToken==null){
//token已经失效了
throw new RuntimeException();
}
Map<String, Object> claims = JwtUtil.parseToken(token);
//把业务数据存储到ThreadLocal中
ThreadLocalUtil.set(claims);
//放行
return true;
} catch (Exception e) {
//http响应状态码为401
response.setStatus(401);
//不放行
return false;
}
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//清空ThreadLocal中的数据
ThreadLocalUtil.remove();
}
}
cofig.webConfig
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
//登录接口和注册接口不拦截
registry.addInterceptor(loginInterceptor).excludePathPatterns("/user/login","/user/register");
}
}
代码展示了一个基于Spring框架的拦截器实现:
LoginInterceptor
类实现了Spring框架的HandlerInterceptor
接口,用于拦截请求并进行登录验证操作。- 在
LoginInterceptor
中,preHandle
方法用于在请求处理之前执行验证操作。它首先获取请求中的名为 “Authorization
“ 的头部信息作为令牌。 - 然后,它通过注入的
StringRedisTemplate
来访问 Redis 存储,检查该令牌是否存在于 Redis 中。如果 Redis 中不存在该令牌,说明令牌已失效,则抛出异常并返回 HTTP 状态码 401。 - 如果令牌有效,它调用
JwtUtil.parseToken(token)
方法来解析令牌,并将业务数据存储到ThreadLocal
中,以便后续请求处理使用。 afterCompletion
方法用于清除ThreadLocal
中的数据,以确保不发生内存泄漏问题。WebConfig
类实现了WebMvcConfigurer
接口,用于配置拦截器。在addInterceptors
方法中,将LoginInterceptor
添加到拦截器注册表中,并排除/user/login
和/user/register
接口,即登录和注册接口不会被该拦截器拦截。
代码的作用是实现了一个基于令牌验证的登录拦截器,用于保护特定的接口不被未认证的用户访问。
5 文件上传
FileUploadController
@RestController
public class FileUploadController {
@PostMapping("/upload")
public Result<String> upload(MultipartFile file) throws Exception {
//把文件的内容存储到本地磁盘上
String originalFilename = file.getOriginalFilename();
//保证文件的名字是唯一的,从而防止文件覆盖
String filename = UUID.randomUUID().toString()+originalFilename.substring(originalFilename.lastIndexOf("."));
String url = AliOssUtil.uploadFile(filename,file.getInputStream());
return Result.success(url);
}
}
AliOssUtil
public class AliOssUtil {
// Endpoint以华东1(杭州)为例,其它Region请按实际情况填写。
private static final String ENDPOINT = "https://oss-cn-beijing.aliyuncs.com";
// 从环境变量中获取访问凭证。运行本代码示例之前,请确保已设置环境变量OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET。
//EnvironmentVariableCredentialsProvider credentialsProvider = CredentialsProviderFactory.newEnvironmentVariableCredentialsProvider();
private static final String ACCESS_KEY_ID="";
private static final String ACCESS_KEY_SECRET="";
// 填写Bucket名称,例如examplebucket。
private static final String BUCKET_NAME = "big-event";
public static String uploadFile(String objectName, InputStream in) throws Exception {
// 创建OSSClient实例。
OSS ossClient = new OSSClientBuilder().build(ENDPOINT,ACCESS_KEY_ID, ACCESS_KEY_SECRET);
String url = "";
try {
// 填写字符串。
String content = "Hello OSS,你好世界";
// 创建PutObjectRequest对象。
PutObjectRequest putObjectRequest = new PutObjectRequest(BUCKET_NAME, objectName, in);
// 上传字符串。
PutObjectResult result = ossClient.putObject(putObjectRequest);
//url组成: https://bucket名称.区域节点/objectName
url = "https://"+BUCKET_NAME+"."+ENDPOINT.substring(ENDPOINT.lastIndexOf("/")+1)+"/"+objectName;
} catch (OSSException oe) {
System.out.println("Caught an OSSException, which means your request made it to OSS, "
+ "but was rejected with an error response for some reason.");
System.out.println("Error Message:" + oe.getErrorMessage());
System.out.println("Error Code:" + oe.getErrorCode());
System.out.println("Request ID:" + oe.getRequestId());
System.out.println("Host ID:" + oe.getHostId());
} catch (ClientException ce) {
System.out.println("Caught an ClientException, which means the client encountered "
+ "a serious internal problem while trying to communicate with OSS, "
+ "such as not being able to access the network.");
System.out.println("Error Message:" + ce.getMessage());
} finally {
if (ossClient != null) {
ossClient.shutdown();
}
}
return url;
}
}