SpringBoot3大事件
效果展示:实战篇-01_实战概述_哔哩哔哩_bilibili
开发模式
前后端分离开发,前后端分别按照同样的接口文档进行开发。接口文档有接口的路径和请求方式,请求参数和响应数据的说明。
环境搭建
-
执行资料中的big_event.sql脚本,准备数据库表
执行SQL指令,创建对应的数据库和表
-
创建springboot工程,引入对应的依赖(web、mybatis、mysql驱动)
<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 http://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.1.3</version> </parent> <groupId>com.ario</groupId> <artifactId>big-event</artifactId> <version>1.0-SNAPSHOT</version> <packaging>jar</packaging> <name>big-event</name> <url>http://maven.apache.org</url> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <!-- web依赖--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- mybatis依赖--> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>3.0.0</version> </dependency> <!-- mysql驱动依赖--> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <version>9.1.0</version> </dependency> </dependencies> </project>
-
配置文件application.yml中引入mybatis的配置信息
-
创建包结构,并准备实体类
用户相关接口
注册接口
可以用lombok方式在实体类中添加get,set方法
常见开发流程
响应类
根据接口文档可以看出,不同请求的响应结果区别不大,可以建立一个同一的响应类,有个响应类,可以很方便的在前后端直接进行json转换。
package com.ario.pojo;
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
//统一响应结果
@NoArgsConstructor
@AllArgsConstructor
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);
}
}
思路分析
写代码依然用三层架构的方式书写
开发
先建立对应的文件
Md5加密算法的使用
工具类
package com.ario.utils;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
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);
}
}
使用
@Override
public void register(String username, String password) {
//加密,密码不要明文存在数据库中,要用工具类加密
String md5String = Md5Util.getMD5String(password);
//添加
userMapper.add(username, md5String);
}
参数校验
对上面的输入信息有要求的话要校验,最直接的方式就是用Java代码进行逻辑判断,如下
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@PostMapping("/register")
public Result register(String username, String password) {
//参数校验,这种方式不推荐
if (username != null && username.length() >= 5 && username.length() <= 16 &&
password != null && password.length() >= 5 && password.length() <= 16) {
//查询用户
User u = userService.findByUserName(username);
if (u == null) {
//没有占用
//注册
userService.register(username, password);
//不用返回带有参数的响应
return Result.success();
} else {
//占用
return Result.error("用户名已被占用");
}
} else {
return Result.error("参数不合法");
}
}
}
当参数多了以后,这种方式就显得非常繁琐了。Spring 提供的一个参数校验框架Spring Validation,使用预定义的注解完成参数校验
使用步骤如下:
-
引入Spring Validation 起步依赖
<!-- validation依赖--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency>
-
在参数前面添加@Pattern注解【当然这里不止有这一个注解】
-
在Controller类上添加@Validated注解
@RestController @RequestMapping("/user") @Validated public class UserController { @Autowired private UserService userService; @PostMapping("/register") //【\\S】表示非空 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("用户名已被占用"); } } }
但是这里报出来的一场,前端收到的信息是异常,缺少描述信息
{ "timestamp": "2025-04-03T13:19:40.856+00:00", "status": 500, "error": "Internal Server Error", "path": "/user/register" }
-
在全局异常处理器中处理参数校验失败的异常
返回信息给前端
@RestControllerAdvice public class GlobalExceptionHandler { //注解的作用是指定要处理的异常 @ExceptionHandler(Exception.class) public Result handleException(Exception e) { e.printStackTrace(); //如果有包装信息,如果没有包装就是返回【操作失败】 return Result.error(StringUtils.hasLength(e.getMessage()) ? e.getMessage() : "操作失败"); } }
返回的信息:
{ "code": 1, "message": "register.username: 需要匹配正则表达式\"^\\S{5,16}$\"", "data": null }
登录接口
@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())) {
//登录成功
return Result.success("jwt token令牌……");
}
return Result.error("密码错误");
}
其中用到service层和mapper层上面已经实现了,所有这里省事儿了。
登录认证
很常见的问题,保护好页面不被未登录的用户所访问。可以用常见的拦截器,这里用jwt令牌
令牌就是一段字符串
-
承载业务数据, 减少后续请求查询数据库的次数
可以存一些公开的用户信息,减少对数据库的查询次数
-
防篡改, 保证信息的合法性和有效性
防止恶意篡改伪造令牌
JWT-概述
- 全称:JSON Web Token (https://jwt.io/)
- 定义了一种简洁的、自包含的格式,用于通信双方以json数据格式安全的传输信息。
- 组成
- 第一部分:Header(头), 记录令牌类型、签名算法等。 例如:
- 第二部分:Payload(有效载荷),携带一些自定义信息、默认信息等。 例如:{"id":"1","username":"Tom"},常用Base64编码表示,注意:这只是一种编码,是公开的,所有不应该保存用户的敏感信息,比如密码
- 第三部分:Signature(签名),防止Token被篡改、确保安全性。将header、payload,并加入指定秘钥,通过指定签名算法计算而来。通过第一部分和第二部分借助密钥,通过加密算法【第一部分指出的】得出的
Base64:是一种基于64个可打印字符(A-Z a-z 0-9 + /)来表示二进制数据的编码方式。
JWT-生成
其实代码是可以自己写的,因为这只是一种规范。这里用别人写好的
-
引入依赖
<!-- java-jwt坐标--> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>4.4.0</version> </dependency> <!-- spring为跟好的测试spring boot。提供了单元测试的坐标--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> </dependency>
-
进行测试
package com.ario; import com.auth0.jwt.JWT; import com.auth0.jwt.algorithms.Algorithm; import org.junit.jupiter.api.Test; import java.util.Date; import java.util.HashMap; import java.util.Map; public class JwtTest { @Test public void testGen() { Map<String, Object> claims = new HashMap<>(); claims.put("id", 1); claims.put("username", "张三"); //生成jwt的代码 String token = JWT.create() .withClaim("user", claims)//添加载荷 .withExpiresAt(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 12))//设置过期时间 .sign(Algorithm.HMAC256("ario"));//指定算法,配置密钥 System.out.println(token); } } //eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjp7ImlkIjoxLCJ1c2VybmFtZSI6IuW8oOS4iSJ9LCJleHAiOjE3NDM4MTM2MDl9.6ddADZl4eU6EqCRKK2o_1j8PrM_NvdX0wOebfkL2o6w
JWT-验证
@Test
public void testParse() {
//定义字符串,模拟用户传递过来的token
String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjp7ImlkIjoxLCJ1c2VybmFtZSI6IuW8oOS4iSJ9LCJleHAiOjE3NDM4MTM2MDl9.6ddADZl4eU6EqCRKK2o_1j8PrM_NvdX0wOebfkL2o6w";
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("ario")).build();//建立应该验证器
DecodedJWT decodedJWT = jwtVerifier.verify(token);//验证token,生成一个解析后的JWT对象
Map<String, Claim> claims = decodedJWT.getClaims();
System.out.println(claims.get("user"));
//如果篡改了头部和载荷部分的数据,那么验证失败
//如果密钥改了,验证失败
//token过期,验证失败
}
- JWT验证时使用的签名秘钥,必须和生成JWT令牌时使用的秘钥是配套的。
- 如果JWT令牌解析时报错,则说明 JWT令牌被篡改 或 失效了,令牌非法。
完善登录认证
-
一般为了减少代码冗余,可以使用工具类
public class JwtUtil { private static final String KEY = "ario"; //接收业务数据,生成token并返回 public static String genToken(Map<String, Object> claims) { return JWT.create() .withClaim("claims", claims) .withExpiresAt(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 12)) .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(); } }
-
登录时生成令牌
@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); return Result.success(token); } return Result.error("密码错误"); }
-
其他接口拦截验证示例
@GetMapping("/list") public Result<String> list(@RequestHeader(name = "Authorization") String token, HttpServletResponse response) { //验证token try { Map<String, Object> claims = JwtUtil.parseToken(token); return Result.success("所有的文章数据……"); } catch (Exception e) { //http响应状态码401 response.setStatus(401); return Result.error("未登录"); } }
使用拦截器
如果每个接口都验证,会很冗余,可以使用连接器,过滤请求
-
编写拦截器
package com.ario.interceptors; import com.ario.utils.JwtUtil; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; import java.util.Map; @Component public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //令牌验证 String token = request.getHeader("Authorization"); //验证token try { Map<String, Object> claims = JwtUtil.parseToken(token); //放行 return true; } catch (Exception e) { //http响应状态码401 response.setStatus(401); //不放行 return false; } } }
-
注册拦截器
com.ario.config中的WebConfig
@Component public class WebConfig implements WebMvcConfigurer { @Autowired private LoginInterceptor loginInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { //登录接口和注册接口不拦截 registry.addInterceptor(loginInterceptor).excludePathPatterns("/user/login", "/user/register"); } }
获取用户详细信息
-
controller层
@GetMapping("/userInfo") public Result<User> userInfo(@RequestHeader(name = "Authorization") String token) { //根据用户名查询用户 Map<String, Object> map = JwtUtil.parseToken(token); String username = (String) map.get("username"); User user = userService.findByUserName(username); return Result.success(user); }
-
service和mapper层已经实现
注意:
-
获得的数据包含了用户的密码等敏感数据,这里要再转到json数据时忽略
在实体类属性上添加注释
注意:导入的是import com.fasterxml.jackson.annotation.JsonIgnore;
@JsonIgnore//让springmvc把当前对象转换成json字符串的时候,忽路password,最终的json字符申中就没有password这个属性了 private String password;//密码
-
得到的数据,时间为空,但是数据库中不为空,这是因为实体类属性名时驼峰命名,而数据库时下划线命名,mybatis转换时对应不起来了。此时配置mybatis,开启驼峰命名转换即可
mybatis: configuration: map-underscore-to-camel-case: true
优化
上面可以看出,我们多层重令牌中解析了用户信息,同时还要传参。这可以用ThreadLocal优化
实现再拦截器中只解析一次。后面直接从ThreadLocal对象中取。
ThreadLocal
提供线程局部变量
- 用来存取数据: set()/get()
- 使用ThreadLocal存储的数据, 线程安全
功能测试
public class ThreadLocalTest {
@Test
public void testThreadLocalSetAndGet(){
//提供一个ThreadLocal对象
ThreadLocal tl = new ThreadLocal();
//开启两个线程
new Thread(()->{
tl.set("萧炎");
System.out.println(Thread.currentThread().getName()+": "+tl.get());
System.out.println(Thread.currentThread().getName()+": "+tl.get());
System.out.println(Thread.currentThread().getName()+": "+tl.get());
},"蓝色").start();
new Thread(()->{
tl.set("药尘");
System.out.println(Thread.currentThread().getName()+": "+tl.get());
System.out.println(Thread.currentThread().getName()+": "+tl.get());
System.out.println(Thread.currentThread().getName()+": "+tl.get());
},"绿色").start();
}
}
/*蓝色: 萧炎
绿色: 药尘
绿色: 药尘
蓝色: 萧炎
绿色: 药尘
蓝色: 萧炎*/
功能实现
这里分装成工具类
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();
}
}
修改拦截器,存数数学,并请求结束后释放空间
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//令牌验证
String token = request.getHeader("Authorization");
//验证token
try {
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();
}
}
Controller直接取就行
@GetMapping("/userInfo")
public Result<User> userInfo(/*@RequestHeader(name = "Authorization") String token*/) {
//根据用户名查询用户
/*Map<String, Object> map = JwtUtil.parseToken(token);
String username = (String) map.get("username");*/
Map<String, Object> map = ThreadLocalUtil.get();
String username = (String) map.get("username");
User user = userService.findByUserName(username);
return Result.success(user);
}
线程之间是不干扰的
更新用户基本信息
代码实现依旧是根据接口文档,通过三层架构的思想
-
controller层
@PutMapping("/update") public Result update(@RequestBody User user) { userService.update(user); return Result.success(); }
-
service层
接口
//更新 void update(User user);
实现类
@Override public void update(User user) { //更新时间 user.setUpdateTime(LocalDateTime.now()); userMapper.update(user); }
-
UserMapper
@Update("update user set nickname=#{nickname},email=#{email},update_time=#{updateTime} where id=#{id}") void update(User user);
实体参数校验
上面注册接口提到的参数校验是传过来的参数。这里是传过来的参数是实体类如何校验呢,
- 可以再实体类上添加注解。
- 再参数列表添加Validated注解使其生效。
下面是常用的几个注解
注解 | 作用 |
---|---|
NotNull | 值不能为null |
NotEmpty | 值不能为null,并且内容不为空 |
满足邮箱格式 | |
Pattern | 自定义限制 |
-
实体类
@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;//更新时间 }
-
controller
@PutMapping("/update") public Result update(@RequestBody @Validated User user) { userService.update(user); return Result.success(); }
更新用户头像
同样三层架构思想。
patch是http里面的局部更新方式
-
controller,注意:这里URL的注解是参数校验,保证参数是合法的URL
@PatchMapping("updateAvatar") public Result updateAvatar(@RequestParam @URL String avatarUrl) { userService.updateAvatar(avatarUrl); return Result.success(); }
-
service
接口类
//更新头像 void updateAvatar(String avatarUrl);
实现类
@Override public void updateAvatar(String avatarUrl) { //获取用户id Map<String, Object> map = ThreadLocalUtil.get(); Integer id = (Integer) map.get("id"); userMapper.updateAvatar(avatarUrl, id); }
-
mapper
@Update("update user set user_pic=#{avatarUrl},update_time=now() where id=#{id}") void updateAvatar(String avatarUrl, Integer id);
更新用户密码
这里用Map传参,而更新用户信息是用User,是因为更新用户时参数和属性这一样springmvc会自动匹配,而这里不一样,springmvc会自动把json数据转换成Map集合
文章分类接口
新增文章分类
-
功能介绍
-
实体类,同时实现参数校验
@Data public class Category { private Integer id;//主键ID @NotEmpty private String categoryName;//分类名称 @NotEmpty private String categoryAlias;//分类别名 private Integer createUser;//创建人ID private LocalDateTime createTime;//创建时间 private LocalDateTime updateTime;//更新时间 }
-
CategoryController,注意这里PostMapping后面没有添加映射路径。这里可以根据请求方式区分
@RestController @RequestMapping("/category") public class CategoryController { @Autowired private CategoryService categoryService; @PostMapping public Result add(@RequestBody Category category) { categoryService.add(category); return Result.success(); } }
-
CategoryService
public interface CategoryService { //新增 void add(Category category); }
CategoryServiceImpl
@Service public class CategoryServiceImpl implements CategoryService { @Autowired private CategoryMapper categoryMapper; @Override public void add(Category category) { //补充属性值 category.setCreateTime(LocalDateTime.now()); category.setUpdateTime(LocalDateTime.now()); Map<String, Object> map = ThreadLocalUtil.get(); Integer userId = (Integer) map.get("id"); category.setCreateUser(userId); categoryMapper.add(category); } }
-
GategoryMapper
@Mapper public interface CategoryMapper { //新增 @Insert("insert into category(category_name,category_alias,create_user,create_time,update_time) " + "values(#{categoryName},#{categoryAlias},#{createUser},#{createTime},#{updateTime})") void add(Category category); }
文章分类列表
-
功能介绍,这里展示出来的分类只是本用户创建的分类
-
Controller
@GetMapping public Result<List<Category>> list() { List<Category> cs = categoryService.list(); return Result.success(cs); }
-
Service
接口
//分类查询 List<Category> list();
实现类
@Override public List<Category> list() { Map<String, Object> map = ThreadLocalUtil.get(); Integer userId = (Integer) map.get("id"); return categoryMapper.list(userId); }
-
Mapper
//分类查询 @Select("select * from category where create_user = #{userId}") List<Category> list(Integer userId);
-
同时为了规范时间格式,可以再实体类中属性上添加注解
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime createTime;//创建时间 @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime updateTime;//更新时间
获取文章分类详情
-
功能介绍
-
controller
@GetMapping("/detail") public Result<Category> detail(Integer id) { Category c = categoryService.findById(id); return Result.success(c); }
-
service
接口
//根据id查询分类 Category findById(Integer id);
实现类
@Override public Category findById(Integer id) { Category c = categoryMapper.findById(id); return c; }
-
mapper
//根据分类id查询分类 @Select("select * from category where id = #{id}") Category findById(Integer id);
更新文章分类
-
功能介绍
-
controller
@PutMapping public Result update(@RequestBody @Validated Category category) { categoryService.update(category); return Result.success(); }
-
service
接口
//跟新分类 void update(Category category);
实现类
@Override public void update(Category category) { category.setUpdateTime(LocalDateTime.now()); categoryMapper.update(category); }
-
mapper
//更新分类 @Update("update category set category_name=#{categoryName},category_alias=#{categoryAlias},update_time=#{updateTime} where id = #{id}") void update(Category category);
-
再实体类上添加注解,实现参数校验
//NotNull是不能不传,NotEmpty只能传非空的字符串 @NotNull private Integer id;//主键ID
分组校验【修复BUG】
上面添加NotNull注解后,是无法添加分类了,因为添加分类时不传递id。
这样让把不同场合的注解作用分开呢——分组校验
分组校验:
把校验项进行归类分组,在完成不同的功能的时候,校验指定组中的校验项
- 定义分组
- 定义校验项时指定归属的分组
- 校验时指定要校验的分组
- 定义校验项时如果没有指定分组,则属于Default分组,分组可以继承
代码详情
-
实体类
@Data public class Category { //NotNull是不能不传,NotEmpty只能传非空的字符串 @NotNull(groups = Update.class) private Integer id;//主键ID @NotEmpty/*(groups = {Add.class, Update.class})*/ private String categoryName;//分类名称 @NotEmpty/*(groups = {Add.class, Update.class})*/ private String categoryAlias;//分类别名 private Integer createUser;//创建人ID @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime createTime;//创建时间 @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime updateTime;//更新时间 //如果说某个校验项没有指定分组,默认属于Default //分组之间可以继承,A extends B 那么A中拥有B中所有的校验项 //这里categoryName和categoryAlias没有指定分组,那么属于Default的分组。下面这两个接口都继承了这个分组,那么这两个分组都有,所有上面就可以注释了 //而id指定了,那么Add分组中就没有这个限制了 public interface Add extends Default { } public interface Update extends Default { } }
-
controller
@PostMapping public Result add(@RequestBody @Validated(Category.Add.class) Category category) { categoryService.add(category); return Result.success(); } @PutMapping public Result update(@RequestBody @Validated(Category.Update.class) Category category) { categoryService.update(category); return Result.success(); }
小结
- 如何定义分组?
- 在实体类内部定义接口
- 如何对校验项分组?
- 通过groups属性指定
- 校验时如何指定分组?
- 给@Validated注解的value属性赋值
- 校验项默认属于什么组?【指明后,就不属于了】
- Default
删除文章分类
全自主实现,easy
-
controller
@DeleteMapping public Result delete(Integer id) { categoryService.delete(id); return Result.success(); }
-
service
接口
//删除 void delete(Integer id);
实现类
@Override public void delete(Integer id) { categoryMapper.delete(id); }
-
mapper
//删除分类 @Delete("delete from category where id = #{id}") void delete(Integer id);
文章管理接口
新增文章
-
功能介绍
和上面两类接口相同,先创建三层文件,和实体类。
先写主逻辑,再参数校验
-
controller
@PostMapping public Result add(@RequestBody Article article) { articleService.add(article); return Result.success(); }
-
service
接口
//新增 void add(Article article);
实现类
@Autowired private ArticleMapper articleMapper; @Override public void add(Article article) { article.setCreateTime(LocalDateTime.now()); article.setUpdateTime(LocalDateTime.now()); Map<String, Object> map = ThreadLocalUtil.get(); Integer UserId = (Integer) map.get("id"); article.setCreateUser(UserId); articleMapper.add(article); }
-
mapper
//新增 @Insert("insert into article(title,content,cover_img,state,category_id,create_user,create_time,update_time) " + "values(#{title},#{content},#{coverImg},#{state},#{categoryId},#{createUser},#{createTime},#{updateTime})") void add(Article article);
参数校验【自定义注解】
先按照接口文档,把常规的参数校验设置上。
private Integer id;//主键ID
@NotEmpty
@Pattern(regexp = "^\\S{1,10}$")
private String title;//文章标题
@NotEmpty
private String content;//文章内容
@NotEmpty
@URL
private String coverImg;//封面图像
@NotEmpty
private String state;//发布状态 已发布|草稿
@NotNull
private Integer categoryId;//文章分类id
private Integer createUser;//创建人ID
private LocalDateTime createTime;//创建时间
private LocalDateTime updateTime;//更新时间
state属性要用到自定义注解。
已有的注解不能满足所有的校验需求,特殊的情况需要自定义校验(自定义校验注解)
- 自定义注解State
- 自定义校验数据的类StateValidation实现ConstraintValidator接口
- 在需要校验的地方使用自定义注解
实现步骤
-
创建自定义注解
package com.ario.anno; import com.ario.validation.StateValidation; import jakarta.validation.Constraint; import jakarta.validation.Payload; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.Target; import static java.lang.annotation.ElementType.*; import static java.lang.annotation.RetentionPolicy.RUNTIME; @Documented//元注解 @Target({ FIELD})//元注解 @Retention(RUNTIME)//元注解 @Constraint(validatedBy = { StateValidation.class})//指定提供校验规则的类 public @interface State { //提供校验失败后的提示信息 String message() default "state参数的值只能是已发布或者草稿"; //指定分组 Class<?>[] groups() default { }; //负载 获取到State注解的附加信息 Class<? extends Payload>[] payload() default { }; }
-
自定义校验数据的类StateValidation实现ConstraintValidator接口
package com.ario.validation; import com.ario.anno.State; import jakarta.validation.ConstraintValidator; import jakarta.validation.ConstraintValidatorContext; //两个参数指定分别是,【给哪个注解提供校验规则】,【校验参数的数据类型】 public class StateValidation implements ConstraintValidator<State,String> { /** * * @param value 将来要校验的数据 * @param context context in which the constraint is evaluated * * @return 如果返回false,则校验不通过,如果返回true,则校验通过 */ @Override public boolean isValid(String value, ConstraintValidatorContext context) { //提供校验规则 if (value == null){ return false; } if (value.equals("已发布") || value.equals("草稿")){ return true; } return false; } }
-
在需要校验的地方使用自定义注解
@State private String state;//发布状态 已发布|草稿
文章列表
-
功能介绍。涉及到分页,动态SQL,动态参数
-
三层架构
代码实现
-
创建PageBean实现类
package com.ario.pojo; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.util.List; //分页返回结果对象 @Data @NoArgsConstructor @AllArgsConstructor public class PageBean <T>{ private Long total;//总条数 private List<T> items;//当前页数据集合 }
-
controller
@GetMapping public Result<PageBean<Article>> list( Integer pageNum, Integer pageSize, @RequestParam(required = false) Integer categoryId, @RequestParam(required = false) String state ) { PageBean<Article> pb = articleService.list(pageNum, pageSize, categoryId, state); return Result.success(pb); }
-
service
接口
//查询 PageBean<Article> list(Integer pageNum, Integer pageSize, Integer categoryId, String state);
用到了PageHelper插件,要导入起步依赖
<!-- spring为跟好的测试spring boot。提供了单元测试的坐标--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> </dependency>
实现类
@Override public PageBean<Article> list(Integer pageNum, Integer pageSize, Integer categoryId, String state) { //1.创建PageBean对象 PageBean<Article> pb = new PageBean<>(); //2.开启分页查询PageHelper PageHelper.startPage(pageNum, pageSize); //3.调用mapper Map<String, Object> map = ThreadLocalUtil.get(); Integer userId = (Integer) map.get("id"); List<Article> as = articleMapper.list(userId, categoryId, state); //Page中提供了方法,可以获取PageHelper分页查询后,得到的总记录条数和当前也数据 Page<Article> p = (Page<Article>) as; //把数据填充PageBean对象中 pb.setTotal(p.getTotal()); pb.setItems(p.getResult()); return pb; }
-
mapper
接口
//查询 List<Article> list(Integer userId, Integer categoryId, String state);
mapper配置文件
在资源文件夹中创建com/airo/mapper。创建ArticleMapper.xml
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.ario.mapper.ArticleMapper"> <!--动态sql--> <select id="list" resultType="com.ario.pojo.Article"> select * from article <where> <if test="categoryId!=null"> category_id=#{categoryId} </if> <if test="state!=null"> and state=#{state} </if> and create_user=#{userId} </where> </select> </mapper>
文章详情、更新,删除
这几个比较简单,放在一起吧
-
controller
@GetMapping("/detail") public Result<Article> detail(Integer id) { Article article = articleService.detail(id); return Result.success(article); } @PutMapping public Result update(@RequestBody @Validated Article article) { articleService.update(article); return Result.success(); } @DeleteMapping public Result delete(Integer id) { articleService.delete(id); return Result.success(); }
-
service
接口
//文章详情 Article detail(Integer id); //文章更新 void update(Article article); //删除文章 void delete(Integer id);
实现类
@Override public Article detail(Integer id) { Article article = articleMapper.detail(id); return article; } @Override public void update(Article article) { article.setUpdateTime(LocalDateTime.now()); articleMapper.update(article); } @Override public void delete(Integer id) { articleMapper.delete(id); }
-
mapper
//文章详情 @Select("select * from article where id = #{id}") Article detail(Integer id); //更新文章 @Update("update article " + "set title=#{title},content=#{content},cover_img=#{coverImg},state=#{state},category_id=#{categoryId},update_time=#{updateTime} " + "where id=#{id}") void update(Article article); //删除文章 @Delete("delete from article where id=#{id}") void delete(Integer id);
其他接口
文件上传
用户头像和文章封面都是要上传图片的
前端三要素
<form action="/upload" method="post" enctype="multipart/form-data"> 头像: <input type="file" name="image"><br> <input type="submit" value="提交"></form>
文件操作MultipartFile
- String getOriginalFilename(); //获取原始文件名
- void transferTo(File dest); //将接收的文件转存到磁盘文件中
- long getSize(); //获取文件的大小,单位:字节
- byte[] getBytes(); //获取文件内容的字节数组
- InputStream getInputStream(); //获取接收到的文件内容的输入流
controller
@RestController
public class FileUploadController {
@PostMapping("/upload")
public Result<String> upload(MultipartFile file) throws IOException {
//把文件的内容存储到本地磁盘上
String originalFilename = file.getOriginalFilename();
//保证文件的名字是唯一的,从而防止文件覆盖
String filename = UUID.randomUUID().toString()+originalFilename.substring(originalFilename.lastIndexOf("."));
file.transferTo(new File("C:\\Users\\Anthony\\Desktop\\test\\"+filename));
return Result.success("url访问地址……");
}
}
上面把图片上传到本地服务器了。存在一些问题
- 无法直接访问
- 磁盘满了
一般是要上传到”云“的
云:互联网上的一堆计算机
阿里云
阿里云是阿里巴巴集团旗下全球领先的云计算公司,也是国内最大的云服务提供商 。
阿里云OSS
阿里云对象存储OSS(Object Storage Service),是一款海量、安全、低成本、高可靠的云存储服务。使用OSS,您可以通过网络随时存储和调用包括文本、图片、音频和视频等在内的各种文件。
第三方服务-通用思路
SDK:Software Development Kit 的缩写,软件开发工具包,包括辅助软件开发的依赖(jar包)、代码示例等,都可以叫做SDK。
阿里云OSS-使用步骤
Bucket:存储空间是用户用于存储对象(Object,就是文件)的容器,所有的对象都必须隶属于某个存储空间。
SDK:Software Development Kit 的缩写,软件开发工具包,包括辅助软件开发的依赖(jar包)、代码示例等,都可以叫做SDK。
代码集成
-
根据官方SDK导入依赖
<!-- 阿里云oss依赖坐标 --> <dependency> <groupId>com.aliyun.oss</groupId> <artifactId>aliyun-sdk-oss</artifactId> <version>3.15.1</version> </dependency> <dependency> <groupId>javax.xml.bind</groupId> <artifactId>jaxb-api</artifactId> <version>2.3.1</version> </dependency> <dependency> <groupId>javax.activation</groupId> <artifactId>activation</artifactId> <version>1.1.1</version> </dependency> <!-- no more than 2.3.3 --> <dependency> <groupId>org.glassfish.jaxb</groupId> <artifactId>jaxb-runtime</artifactId> <version>2.3.3</version> </dependency>
-
测试类验证
package com.ario; import com.aliyun.oss.ClientException; import com.aliyun.oss.OSS; import com.aliyun.oss.OSSClientBuilder; import com.aliyun.oss.OSSException; import com.aliyun.oss.model.PutObjectRequest; import com.aliyun.oss.model.PutObjectResult; import java.io.FileInputStream; public class Demo { public static void main(String[] args) throws Exception { // Endpoint以华东1(杭州)为例,其它Region请按实际情况填写。 String endpoint = "https://oss-cn-beijing.aliyuncs.com"; // 从环境变量中获取访问凭证。运行本代码示例之前,请确保已设置环境变量OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET。 //EnvironmentVariableCredentialsProvider credentialsProvider = CredentialsProviderFactory.newEnvironmentVariableCredentialsProvider(); String ACCESS_KEY_ID="你的ID"; String ACCESS_KEY_SECRET="你的密钥"; // 填写Bucket名称,例如examplebucket。 String bucketName = "big-event343"; // 填写Object完整路径,完整路径中不能包含Bucket名称,例如exampledir/exampleobject.txt。 String objectName = "001.png"; // 创建OSSClient实例。 OSS ossClient = new OSSClientBuilder().build(endpoint,ACCESS_KEY_ID, ACCESS_KEY_SECRET); try { // 填写字符串。 String content = "Hello OSS,你好世界"; // 创建PutObjectRequest对象。 PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, objectName, new FileInputStream("D:\\Pictures\\头像等\\头像.jpg")); // 如果需要上传时设置存储类型和访问权限,请参考以下示例代码。 // ObjectMetadata metadata = new ObjectMetadata(); // metadata.setHeader(OSSHeaders.OSS_STORAGE_CLASS, StorageClass.Standard.toString()); // metadata.setObjectAcl(CannedAccessControlList.Private); // putObjectRequest.setMetadata(metadata); // 上传字符串。 PutObjectResult result = ossClient.putObject(putObjectRequest); } 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(); } } } }
上传成功,在阿里云可以看到,生成了Url:https://big-event343.oss-cn-beijing.aliyuncs.com/001.png
可以观察到地址是有规律的。后面可以用这个规律,在后端代码中编写url地址
-
上面大多数代码都是固定的,可以集成到工具类中
package com.ario.utils; import com.aliyun.oss.ClientException; import com.aliyun.oss.OSS; import com.aliyun.oss.OSSClientBuilder; import com.aliyun.oss.OSSException; import com.aliyun.oss.model.PutObjectRequest; import com.aliyun.oss.model.PutObjectResult; import java.io.FileInputStream; import java.io.InputStream; 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="你的ID"; private static final String ACCESS_KEY_SECRET="你的密钥"; // 填写Bucket名称,例如examplebucket。 private static final String BUCKET_NAME = "big-event343"; 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); // 如果需要上传时设置存储类型和访问权限,请参考以下示例代码。 // ObjectMetadata metadata = new ObjectMetadata(); // metadata.setHeader(OSSHeaders.OSS_STORAGE_CLASS, StorageClass.Standard.toString()); // metadata.setObjectAcl(CannedAccessControlList.Private); // putObjectRequest.setMetadata(metadata); // 上传字符串。 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; } }
-
controller修改代码逻辑,使用阿里云
@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(".")); // file.transferTo(new File("C:\\Users\\Anthony\\Desktop\\test\\"+filename)); String url = AliOssUtil.uploadFile(filename,file.getInputStream()); return Result.success(url); } }
登录优化-redis
上面关于用户接口时密码修改后但是令牌没有失效,旧的令牌依然可以使用。
这里可以用redis优化
令牌主动失效机制
- 登录成功后,给浏览器响应令牌的同时,把该令牌存储到redis中
- LoginInterceptor拦截器中,需要验证浏览器携带的令牌,并同时需要获取到redis中存储的与之相同的令牌
- 当用户修改密码成功后,删除redis中存储的旧令牌
如果对redis不熟悉,可以通过下面视频学习redis黑马
安装redis【windows版】
这里资料已经下载了,直接点击就能用
SpringBoot集成redis
-
导入spring-boot-starter-data-redis起步依赖
<!-- redis坐标--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
-
在yml配置文件中, 配置redis连接信息
data: redis: host: localhost port: 6379
-
调用API(StringRedisTemplate)完成字符串的存取操作
@SpringBootTest //如果在测试类上添加了这个注解,那么将来单元测试方法执行之前,会先初始化Spring容器 public class RedisTest { //依赖中已经自动注入对象了,这里直接用就行 @Autowired private StringRedisTemplate stringRedisTemplate; @Test public void testSet() { //往redis中存储一个键值对 StringRedisTemplate ValueOperations<String, String> operations = stringRedisTemplate.opsForValue(); operations.set("username", "zhangsan"); //可以设置过期时间 operations.set("id", "1", 15, TimeUnit.SECONDS); } @Test public void testGet() { //从redis中获取一个键值对 StringRedisTemplate ValueOperations<String, String> operations = stringRedisTemplate.opsForValue(); System.out.println(operations.get("username")); } }
令牌主动失效机制
-
登录成功后,给浏览器响应令牌的同时,把该令牌存储到redis中
@Autowired private StringRedisTemplate stringRedisTemplate; @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, 12, TimeUnit.HOURS); return Result.success(token); } return Result.error("密码错误"); }
-
LoginInterceptor拦截器中,需要验证浏览器携带的令牌,并同时需要获取到redis中存储的与之相同的令牌
@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; } }
-
当用户修改密码成功后,删除redis中存储的旧令牌
@PatchMapping("/updatePwd") public Result updatePwd(@RequestBody Map<String, String> params,@RequestHeader(name = "Authorization") String token) { //1.校验参数 String oldPwd = params.get("old_pwd"); String newPwd = params.get("new_pwd"); String rePwd = params.get("re_pwd"); //StringUtils是spring提供的方法 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 ,这里的token通过参数传递过来 ValueOperations<String, String> operations = stringRedisTemplate.opsForValue(); operations.getOperations().delete(token); return Result.success(); }
项目部署
打包运行
注意:jar包部署,要求服务器必须有jre环境
项目开放完毕之后虽然可以通过idea运行。但是不是一致打开的。所有要放到服务器上面完成部署。
这里为了方便,以Windows环境为例进行项目部署
-
先加入打包插件
<build> <plugins> <!-- 打包插件 --> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <version>3.1.3</version> </plugin> </plugins> </build>
-
开始打包
这里突然报错了
Please refer to D:\code\javacode\Projects\big-event\target\surefire-reports for the individual test results. Please refer to dump files (if any exist) [date].dump, [date]-jvmRun[N].dump and [date].dumpstream.
查看对应目录下面文件以后发现redis连接失败,应该是我把redis关了的原因。
结果发现redis连接成功了,然后报token失效了。发现在测试类中有token死代码。跟新以后没问题了。当然也可以跳过测试类进行打包
这是可以把jar包传到服务器上面了。
-
然后在对应目录的命令行中执行运行指令
java -jar big-event-1.0-SNAPSHOT.jar
-
运行成功后就可以正常访问了。如果要停止运行的话输入Ctrl + C快速停止
注意:运行时可能出现端口冲突问题而报错
属性配置方式
面对上面的端口报错问题,对于运维人员或客户时没有办法更改项目中的配置文件的,因为已经打包了。
下面时几个常见的属性配置方式
-
命令行参数方式
-
语法:--键=值。例如--server.port=10010
-
原理:这个参数时通过args参数传递的
-
-
环境变量方式
注意:想要让环境变量起效,要重新打开命令行。
-
外部配置文件方式
那么这几种方式的优先级是什么呢?想想也可以看出。在上面问题的解决中其他方式会覆盖掉项目内部的配置。完整的优先级如下
项目中resources目录下的application.yml < LJar包所在目录下的application.yml < 操作系统环境变量 < 命令行参数
多环境开发-Profiles
项目可能在开放、测试和生成等个环境开发,以数据库为例,不同环境的数据库也不一样,要不断更改配置文件吗?这样造成了修改繁琐,容易出错的缺点
单文件配置
SpringBoot提供的Profiles可以用来隔离应用程序配置的各个部分,并在特定环境下指定某些部分的配置生效
-
如何分隔不同环境的配置?
---
-
如何指定哪些配置属于哪个环境?如何指定哪个环境的配置生效?
#通用信息,指定生效的环境 #多环境下共性的属性 #如果特定环境中的配置和通用信息冲突了,特定环境中的配置生效【局部优于全局】 spring: profiles: active: dev --- #开发环境 spring: config: activate: on-profile: dev server: port: 8081 --- #测试环境 spring: config: activate: on-profile: test server: port: 8082 --- #生成环境 spring: config: activate: on-profile: pro server: port: 8083
多文件配置
当配置较多时,维护起来很杂乱。可以分成多个文件单独维护
小结
-
单文件配置
-
--- 分隔不同环境的配置
-
spring.config.activate.on-profile 配置所属的环境
-
spring.profiles.active 激活环境
-
-
多文件配置
- 通过多个文件分别配置不同环境的属性
- 文件的名字为 application-环境名称.yml
- 在application.yml中激活环境
分组
当配置信息很多时,可以根据配置的种类在进行拆分成单独文件,这样便于管理