Springboot后端实战篇
注册
引入Spring Validation依赖
<dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-validation</artifactId > </dependency >
导入Md5Util工具类
package com.zjf.utils;import java.security.MessageDigest;import java.security.NoSuchAlgorithmException;public class Md5Util { 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(); } } public static String getMD5String (String s) { return getMD5String(s.getBytes()); } 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 ]; char c1 = hexDigits[bt & 0xf ]; stringbuffer.append(c0); stringbuffer.append(c1); } }
三层架构
UserController
package com.zjf.controller;@Validated @RestController @RequestMapping("/user") public class UserController { @Autowired UserService userservice; @PostMapping("/register") public Result register (@Pattern(regexp = "^\\S{5,16}$") String username, @Pattern(regexp = "^\\S{5,16}$") String password) { if (userService.findByUserName(username) != null ) { return Result.error("用户名已存在" ); } else { userService.register(username, password); return Result.success(); } } }
UserServiceImpl
package com.zjf.service.impl;import com.zjf.utils.Md5Util;@Service public class UserServiceImpl implements UserService { @Autowired UserMapper userMapper; @Override public void register (String username, String password) { String md5String = Md5Util.getMD5String(password); userMapper.insertUser(username,md5String); } @Override public User findByUserName (String username) { return userMapper.findByUserName(username); } }
UserMappper
package com.zjf.mapper;@Mapper public interface UserMapper { @Insert("insert into user(username,password,create_time,update_time)" + " values(#{username},#{password},now(),now())") void insertUser (String username, String password) ; @Select("select * from user where username=#{username}") User findByUserName (String username) ; }
知识要点
Md5加密
导入Md5Utils工具类
将输入的密码使用Md5Util类进行MD5加密生成md5String
@Override public void register (String username, String password) { String md5String = Md5Util.getMD5String(password); userMapper.insertUser(username,md5String); }
参数校验
使用 Spring Validation, 对注册接口的参数进行合法性校验
引入 Spring Validation 起步依赖
<dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-validation</artifactId > </dependency >
在参数前面添加 @Pattern 注解
register(@Pattern(regexp = "^\\S{5,16}$") String username, @Pattern(regexp = "^\\S{5,16}$") String password)
在 Controller 类上添加 @Validated 注解
在全局异常处理器中处理参数校验失败的异常
当用户名和密码的长度不满足5~16位非空字符时,参数校验失败,服务器抛出错误,“status”: 500, “error”: “Internal Server Error”,表示服务器遇到了一个内部错误。此时提供一个全局的异常处理机制。当应用程序中的任何控制器方法抛出异常时,handleException
方法会被调用。它会打印异常的堆栈跟踪信息,并返回一个包含错误信息的 Result
对象给客户端。这种方式有助于统一异常处理逻辑,提高代码的可维护性。
package com.zjf.exception;@RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(Exception.class) public Result handleException (Exception e) { e.printStackTrace(); return Result.error(StringUtils.hasLength(e.getMessage())? e.getMessage() : "操作失败" ); } }
登录
引入jwt依赖
<dependency > <groupId > com.auth0</groupId > <artifactId > java-jwt</artifactId > <version > 4.4.0</version > </dependency >
导入工具类jwtUtil
package com.zjf.utils;import com.auth0.jwt.JWT;import com.auth0.jwt.algorithms.Algorithm;import java.util.Date;import java.util.Map;public class JwtUtil { private static final String KEY = "itheima" ; 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)); } public static Map<String, Object> parseToken (String token) { return JWT.require(Algorithm.HMAC256(KEY)) .build() .verify(token) .getClaim("claims" ) .asMap(); } }
编写UserController
@PostMapping("/login") public Result login ( @Pattern(regexp = "^\\S{5,16}$") String username, @Pattern(regexp = "^\\S{5,6}$") String password) { User loginUser = userService.findByUserName(username); if (loginUser == null ) { return Result.error("用户名错误" ); } 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("密码错误" ); }
登录拦截器
LoginInterceptor
LoginInterceptor
是一个用于验证用户是否登录的拦截器。它通过检查HTTP请求中的JWT来验证用户的身份,如果JWT有效,则允许请求继续;如果JWT无效,则拒绝请求。
package com.zjf.interceptors;@Component public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle (HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("Authorization" ); try { Map<String, Object> claims = JwtUtil.parseToken(token); return true ; } catch (Exception e) { response.setStatus(401 ); return false ; } } }
WebConfig
WebConfig是一个配置类,用于注册一个登录拦截器(LoginInterceptor)。通过实现Spring MVC的WebMvcConfigurer接口,并重写addInterceptors方法,将登录拦截器添加到拦截器注册表中。在拦截器的配置中,指定了不拦截登录接口和注册接口的路径,即对路径为/user/login和/user/register的请求不进行拦截
package com.zjf.config;import com.zjf.interceptors.LoginInterceptor;import com.zjf.interceptors.LoginInterceptor;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.context.annotation.Configuration;import org.springframework.web.servlet.config.annotation.InterceptorRegistry;import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;@Configuration public class WebConfig implements WebMvcConfigurer { @Autowired private LoginInterceptor loginInterceptor; @Override public void addInterceptors (InterceptorRegistry registry) { registry.addInterceptor(loginInterceptor).excludePathPatterns("/user/login" ,"/user/register" ); } }
知识要点
JWT
JSON Web Token(JWT)是一种开放标准(RFC 7519),用于在各方之间安全地传输信息作为 JSON 对象。JWT 可以通过数字签名进行验证,因此是一种非常安全的方式,通常用于在身份验证(Authentication)和信息交换(Information Exchange)方面。
JWT 结构
JWT 由三部分组成,通过点(.)分隔:
Header(头部) : 包含了令牌的元数据信息,例如算法(HS256、RS256、HMAC256)、令牌类型(JWT)等。
Payload(负载) : 包含了被加密的数据,例如用户的身份信息或其他声明。
Signature(签名) : 使用头部指定的算法和密钥对头部和负载进行签名,确保数据的完整性和认证。
这三部分通过Base64 URL编码后连接在一起,形成最终的JWT令牌。
JWT 使用
JWT 的主要使用场景是在客户端和服务器之间安全地传递信息,特别是在身份验证过程中。一般的流程如下:
认证流程 :
用户向服务器提供用户名和密码进行身份验证。
服务器验证用户的身份,并生成包含用户信息的JWT。
服务器将生成的JWT返回给客户端。
请求认证 :
客户端在以后的请求中将JWT放置在请求头部的Authorization字段中,通常格式为 Authorization: Bearer <token>
。
服务器接收到请求后,解析JWT并验证签名的有效性。
如果验证成功,服务器处理请求;如果验证失败或者JWT过期,服务器拒绝请求。
拦截器
拦截器(Interceptor)是一种动态拦截方法调用的机制,在SpringMVC中动态拦截控制器方法的执行,是java中AOP 思想的运用。
创建拦截器
在Spring框架中,拦截器是通过实现HandlerInterceptor
接口来创建的。这个接口定义了三个方法,分别对应请求处理的不同阶段:
preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
:这个方法在请求实际处理之前被调用。在这个方法中,可以执行一些前置处理,如身份验证、权限检查等。如果这个方法返回true
,则请求会继续执行;如果返回false
,则请求将被终止。
postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)
:这个方法在请求实际处理之后,但在视图渲染之前被调用。在这个方法中,可以对模型的属性进行修改,或者添加额外的模型数据。
afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
:这个方法在整个请求完成后被调用,即在视图渲染之后。在这个方法中,可以执行一些清理工作,如释放资源、记录日志等。
配置拦截器
创建一个配置类,实现WebMvcConfigurer
接口,并重写addInterceptors
方法。在这个方法中,你可以使用InterceptorRegistry
来注册你的拦截器,并指定拦截器应该作用的路径或者排除的路径。
注册拦截器
在addInterceptors
方法中,使用InterceptorRegistry
的addInterceptor
方法来注册你的拦截器。然后,你可以使用addPathPatterns
来指定拦截器应该拦截哪些路径,或者使用excludePathPatterns
来指定拦截器不应该拦截哪些路径。
获取用户详细信息
编写UserController
注意:/userInfo接口不携带参数username,token令牌中存放了用户的id和usern,通过携带token令牌,获取username。@RequestHeader
注解用于将请求头中的信息绑定到控制器方法参数上。当你在方法参数前面加上@RequestHeader
注解,并指定name
属性时,Spring会自动将请求中名为Authorization
的头部的值赋给该参数。当HTTP请求到达服务器并调用这个方法时,Spring会查找请求头中名为Authorization
的字段,并将其值作为字符串参数authorizationHeader
传递给方法。
@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); }
测试结果
在测试结果中,用户的密码password同样也响应给客户端。密码通常是敏感信息,不应该在网络请求的响应中返回给客户端。这可能导致用户隐私泄露的问题。在设计和实现时,应实现将密码等敏感信息从序列化的过程中排除出去。
@JsonIgnore
是Jackson库中的一个注解,它用于在将Java对象序列化为JSON格式时忽略特定的属性。当你想要隐藏某些敏感信息或不希望这些信息被客户端看到时,比如密码、加密密钥等。在Java类的某个字段或方法上添加@JsonIgnore
注解,Jackson在序列化过程中就会跳过这个字段或方法,不会将其包含在生成的JSON中。
在本例中User类的password字段上加上**@JsonIgnore**,实现在被序列化成 JSON 时,password
字段将会被忽略,不会出现在生成的 JSON 中。
@JsonIgnore private String password;
测试结果
ThreadLocal
ThreadLocal
,即线程本地变量,提供线程局部变量。如果你创建了一个ThreadLocal
变量,那么访问这个变量的每个线程都会有这个变量的一个本地拷贝,多个线程操作这个变量的时候,实际是在操作自己本地内存里面的变量,从而起到线程隔离 的作用,避免了并发场景下的线程安全问题 。
用来存取数据 : set()/get()
使用 ThreadLocal 存储的数据 , 线程安全
用完记得调用 remove 方法释放
在之前的代码中,我们使用**@RequestHeader**注解获取请求头中名为Authorization
的头部的值,并使用JwtUtil工具类来解析token,获取其中的username。如果存在多个这样的接口,相应的会重复编写这串代码。通过拦截器中通一的解析token实现代码的复用
导入ThreadLocal 工具类
package com.zjf.utils;import java.util.HashMap;import java.util.Map;@SuppressWarnings("all") public class ThreadLocalUtil { 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); } 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" ); try { Map<String, Object> claims = JwtUtil.parseToken(token); ThreadLocalUtil.set(claims); return true ; } catch (Exception e) { response.setStatus(401 ); return false ; } } @Override public void afterCompletion (HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { ThreadLocalUtil.remove(); } }
修改/userInfo接口
@GetMapping("/userInfo") public Result<User> userInfo () { Map<String, Object> map = ThreadLocalUtil.get(); String username = (String) map.get("username" ); User user = userService.findByUserName(username); return Result.success(user); }
更新用户基本信息
三层架构
UserController
@PutMapping("/update") public Result update (@RequestBody User user) { userService.update(user); return Result.success(); }
UserServiceImpl
@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) ;
参数校验
对上述的更新用户基本信息update代码进行参数校验,不同与之前注册时对属性的参数,update方法传入的参数是一个User对象
实体参数校验
实体类的成员变量上添加注解
@NotNull
@NotEmpty
@Email
接口方法的实体参数上添加 @Validated 注解
更新用户头像
三层架构
UserController
@PatchMapping("/updateAvatar") public Result updateAvatar (@RequestParam @URL String avatarUrl) { userService.updateAvatar(avatarUrl); return Result.success(); }
UserServiceImpl
@Override public void updateAvatar (String avatarUrl) { Map<String, Object> map = ThreadLocalUtil.get(); Integer id = (Integer) map.get("id" ); userMapper.updateAvatar(avatarUrl,id); }
UserMapper
@Update("update user set user_pic = #{userPic},update_time = NOW() where id = #{id}") void updateAvatar (String userPic,Integer id) ;
知识要点
参数校验
@URL注解,通常用于参数验证,表示该参数必须是一个合法的URL格式。
public Result updateAvatar (@RequestParam @URL String avatarUrl)
@PatchMapping
@PatchMapping
注解用于将 HTTP PATCH 请求映射到指定的处理方法。HTTP PATCH 方法通常用于更新资源的部分内容,而不是替换整个资源。@PatchMapping
用于部分更新资源,而 @PutMapping
用于替换整个资源。@PatchMapping
请求仅包含要更新的部分内容,而 @PutMapping
请求包含完整的资源表述。
更新用户密码
三层架构
Usercontroller
@PatchMapping("/updatePwd") public Result updatePwd (@RequestBody Map<String,String> params) { String oldPwd = params.get("old_pwd" ); String newPwd = params.get("new_pwd" ); String re_pwd = params.get("re_pwd" ); if (!StringUtils.hasLength(oldPwd)||!StringUtils.hasLength(newPwd)||!StringUtils.hasLength(re_pwd)){ return Result.error("参数错误" ); } Map<String,Object> map= ThreadLocalUtil.get(); String username = (String) map.get("username" ); User loginUser = userService.findByUserName(username); if (!Md5Util.getMD5String(oldPwd).equals(loginUser.getPassword())){ return Result.error("旧密码错误" ); } if (Md5Util.getMD5String(newPwd).equals(loginUser.getPassword())){ return Result.error("新密码不能与旧密码一致" ); } if (!newPwd.equals(re_pwd)){ return Result.error("两次密码不一致" ); } userService.updatePwd(newPwd); return Result.success(); }
UserService
@Override public void updatePwd (String newPwd) { Map<String, Object> map = ThreadLocalUtil.get(); Integer id = (Integer) map.get("id" ); String md5String = Md5Util.getMD5String(newPwd); userMapper.updatePwd(md5String,id); }
UserMapper
@Update("update user set password = #{newPwd},update_time = now() where id = #{id}") void updatePwd (String newPwd, Integer id) ;
新增文章分类
三层架构
CategoryController
package com.zjf.controller;@RestController @RequestMapping("/category") public class CategoryController { @Autowired private CategoryService categoryService; @PostMapping public Result add (@RequestBody @Validated Category category) { categoryService.add(category); return Result.success(); } }
CategoryServiceImpl
package com.zjf.service.impl;@Service public class CategoryServiceImpl implements CategoryService { @Autowired private CategoryMapper categoryMapper; @Override public void add (Category category) { Map<String,Object> map = ThreadLocalUtil.get(); Integer id = (Integer) map.get("id" ); category.setCreateTime(LocalDateTime.now()); category.setUpdateTime(LocalDateTime.now()); category.setCreateUser(id); categoryMapper.add(category); } }
CategoryMapper
@Mapper public interface CategoryMapper { @Insert("insert into category(category_name, category_alias, create_user, create_time, update_time)" + " VALUE(#{categoryName},#{categoryAlias},#{createUser},#{createTime},#{updateTime})") void add (Category category) ; }
知识要点
参数校验
未进行控制层的参数校验之前,测试时如果传入categoryName或categoryName为空,系统会在SQL层抛出错误,通过参数校验,实现controller层抛出错误
1.在Category实体类 categoryName, categoryName字段上添加@NotEmpty注解
@NotEmpty private String categoryName;@NotEmpty private String categoryName;
2.在控制层对应方法参数前加上@Validated注解
public Result add (@RequestBody @Validated Category category)
文章分类列表
三层架构
CategoryController
@GetMapping public Result<List<Category>> list () { List<Category> cs = categoryService.list(); return Result.success(cs); }
CategoryServiceImpl
public List<Category> list () { Map<String,Object> map = ThreadLocalUtil.get(); Integer id = (Integer) map.get("id" ); return categoryMapper.list(id); }
CategoryMapper
@Select("select * from category where create_user = #{id}") List<Category> list (Integer id) ;
知识要点
@JsonFormat注解
用于格式化Java对象中的日期和时间属性,当这个对象被转换为JSON字符串时,会按照指定的格式来格式化日期和时间
private LocalDateTime createTime;@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime updateTime;
测试结果:
{ "id" : 1 , "categoryName" : "美食" , "categoryAlias" : "ms" , "createUser" : 10 , "createTime" : "2024-06-18T17:29:26" , "updateTime" : "2024-06-18 17:29:26" }
获取文章分类详情
三层架构
CategoryController
@GetMapping("/detail") public Result<Category> detail (Integer id) { Category category = categoryService.detail(id); return Result.success(category); }
CategoryServiceImpl
@Override public Category detail (Integer id) { return categoryMapper.findById(id); }
CategoryMapper
@Select("select * from category where id = #{id}") Category findById (Integer id) ;
更新文章分类
三层架构
CategoryController
@PutMapping public Result update (@RequestBody @Validated(Category.Update.class) Category category) { categoryService.update(category); return Result.success();
CategoryServiceImpl
@Override public void update (Category category) { category.setUpdateTime(LocalDateTime.now()); categoryMapper.update(category); }
CategoryMapper
@Update("update category set category_name=#{categoryName},category_alias=#{categoryAlias},update_time=#{updateTime} where id=#{id}") void update (Category category) ;
知识要点
参数校验
更新文章分类参数说明:
参数名称
说明
类型
是否必须
备注
id
主键ID
number
是
categoryName
分类名称
string
是
categoryAlias
分类别名
string
是
id、categoryName、categoryAlias均为必须,故在实体类Category类的相关字段上添加相应注解
@NotNull private Integer id;@NotEmpty private String categoryName;@NotEmpty private String categoryAlias;
注意 :在新增文章分类中,已经为categoryName、categoryAlias添加了@NotEmpty注解。通过给id、categoryName、categoryAlias添加注解,以及为控制层方法参数添加@Validated注解,实现了更新文章分类的参数校验,但我们注意到在新增文章分类方法中,也为相同的参数category添加了@Validated参数校验。此时,在执行新增文章分类时,会抛出id 不能为null的错误。为解决此问题,就要用到分组校验。
分组校验
把校验项进行归类分组,在完成不同的功能的时候,校验指定组中的校验项
定义分组
在category实体类中定义了两个空接口Add和Update,用于分组校验
public class Category {public interface Add {} public interface Update {} }
定义校验项时指定归属的分组
@NotNull(groups = Update.class) private Integer id;@NotEmpty(groups = {Add.class, Update.class}) private String categoryName;@NotEmpty(groups = {Add.class, Update.class}) private String categoryAlias;
校验时指定要校验的分组
public Result update (@RequestBody @Validated(Category.Update.class) Category category) {}
public Result add (@RequestBody @Validated(Category.Add.class) Category category) {}
注:
定义校验时,如果没有指定分组,默认属于default组
分组之间可以继承,A extends B,那么A用有B中所有的校验项
上述步骤2.定义校验项时指定归属的分组,可简化如下
@NotNull(groups = Update.class) private Integer id;@NotEmpty private String categoryName;@NotEmpty private String categoryAlias;
删除文章分类
三层架构
CategoryController
@DeleteMapping public Result delete (Integer id) { categoryService.delete(id); return Result.success(); }
CategoryServiceImpl
@Override public void delete (Integer id) { categoryMapper.delete(id); }
CategoryMapper
@Delete("delete from category where id=#{id}") void delete (Integer id) ;
新增文章
三层架构
ArticleController
@RestController @RequestMapping("/article") public class ArticleController { @Autowired private ArticleService articleService;; @PostMapping public Result add (@RequestBody Article article) { articleService.add(article); return Result.success(); } }
ArticleServiceImpl
@Service public class ArticleServiceImpl implements ArticleService { @Autowired private ArticleMapper articleMapper; @Override public void add (Article article) { Map<String,Object> map = ThreadLocalUtil.get(); Integer id = (Integer) map.get("id" ); article.setCreateUser(id); article.setCreateTime(LocalDateTime.now()); article.setUpdateTime(LocalDateTime.now()); articleMapper.add(article); } }
ArticleMapper
@Mapper public interface ArticleMapper { @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) ; }
知识要点
参数校验
新增文章请求参数说明:
参数名称
说明
类型
是否必须
备注
title
文章标题
string
是
1~10个非空字符
content
文章正文
string
是
coverImg
封面图像地址
string
是
必须是url地址
state
发布状态
string
是
已发布 | 草稿
categoryId
文章分类ID
number
是
自定义参数校验
已有的注解不能满足所有的校验需求,特殊的情况需要自定义校验 ( 自定义校验注解 )
自定义注解 State
package com.zjf.anno;@Documented @Target({FIELD}) @Retention(RUNTIME) @Constraint(validatedBy = {StateVlidation.class}) public @interface State { String message () default "{state参数的值只能是已发布或草稿}" ; Class<?>[] groups() default {}; Class<? extends Payload >[] payload() default {}; }
自定义校验数据的类 StateValidation实现 ConstraintValidator 接口
package com.zjf.validation;public class StateVlidation implements ConstraintValidator <State,String> { @Override public boolean isValid (String value, ConstraintValidatorContext context) { if (value==null ){ return false ; } if (value.equals("已发布" )||value.equals("草稿" )){ return true ; } return false ; } }
在需要校验的地方使用自定义注解
public class Article { private Integer id; @NotEmpty @Pattern(regexp = "^\\S{1,10}$") private String title; @NotEmpty private String content; @NotEmpty @URL private String coverImg; @State private String state; @NotNull private Integer categoryId; private Integer createUser; private LocalDateTime createTime; private LocalDateTime updateTime; }
文章列表 ( 条件分页 )
引入pagehelper依赖
<dependency > <groupId > com.github.pagehelper</groupId > <artifactId > pagehelper-spring-boot-starter</artifactId > <version > 1.4.6</version > </dependency >
导入PageBean实体
package com.zjf.pojo;@Data @NoArgsConstructor @AllArgsConstructor public class PageBean <T>{ private Long total; private List<T> items; }
三层架构
ArticleController
@GetMapping public Result<PageBean<Article> > list(Integer pageNum, Integer pageSize, @RequestParam(required = false) Integer categoryId, @RequestParam(required = false) String state){ PageBean<Article> pageBean = articleService.list(pageNum,pageSize,categoryId,state); return Result.success(pageBean); }
ArticleServiceImpl
@Override public PageBean<Article> list (Integer pageNum, Integer pageSize, Integer categoryId, String state) { PageBean<Article> pb = new PageBean <>(); PageHelper.startPage(pageNum, pageSize); Map<String, Object> map = ThreadLocalUtil.get(); Integer id = (Integer) map.get("id" ); List<Article> as = articleMapper.list(categoryId, state, id); Page<Article> p = (Page<Article>) as; pb.setTotal(p.getTotal()); pb.setItems(p.getResult()); return pb; }
ArticleMapper
List<Article> list (Integer categoryId, String state, Integer id) ;
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.zjf.mapper.ArticleMapper" > <select id ="list" resultType ="com.zjf.pojo.Article" > select * from article <where > <if test ="categoryId!=null" > category_id=#{categoryId} </if > <if test ="state!=null and state!=''" > and state=#{state} </if > and create_user=#{id} </where > </select > </mapper >
知识要点
@RequestParam
@RequestParam 注解是将请求参数绑定到控制器的方法参数上
1、语法: @RequestParam(value=”参数名”,required=”true/false”,defaultValue=””)
2、属性:
value :表示参数名,即前端页面传过来的参数名
defaultValue :参数默认值,如果设置了该值,required=true将失效,自动为false,如果没有传该参数,就使用默认值
required :表示是否要强制包含该参数,默认值为false,表示允许请求中不包含该参数,并且该参数值会为设为null。true表示该请求中必须包含该参数否则报错
动态SQL
动态 SQL_MyBatis中文网
获取文章详情
三层架构
ArticleController
@GetMapping("/detail") public Result detail (Integer id) { Article article = articleService.findById(id); return Result.success(article); }
ArticleServiceImpl
@Override public Article findById (Integer id) { Article article = articleMapper.findById(id); return article; }
ArticleMapper
@Select("select * from article where id=#{id}") Article findById (Integer id) ;
参数校验
响应参数说明:
名称
类型
是否必须
默认值
备注
其他信息
code
number
必须
响应码, 0-成功,1-失败
message
string
非必须
提示信息
data
object
必须
返回的数据
|-id
number
非必须
主键ID
|-title
string
非必须
文章标题
|-content
string
非必须
文章正文
|-coverImg
string
非必须
文章封面图像地址
|-state
string
非必须
发布状态
已发布|草稿
|-categoryId
number
非必须
文章分类ID
|-createTime
string
非必须
创建时间
|-updateTime
string
非必须
更新时间
响应数据样例:
{ "code" : 0 , "message" : "操作成功" , "data" : { "id" : 4 , "title" : "北京旅游攻略" , "content" : "天安门,颐和园,鸟巢,长城...爱去哪去哪..." , "coverImg" : "https://big-event-gwd.oss-cn-beijing.aliyuncs.com/9bf1cf5b-1420-4c1b-91ad-e0f4631cbed4.png" , "state" : "已发布" , "categoryId" : 2 , "createTime" : "2023-09-03 11:35:04" , "updateTime" : "2023-09-03 11:40:31" } }
使用@JsonIgnore 注解在序列化和反序列化时忽略createUser字段;使用@JsonFormat(pattern = “yyyy-MM-dd HH:mm:ss”) 注解在序列化和反序列化时,按照指定的日期时间格式进行处理。
@JsonIgnore private Integer createUser;@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime createTime;@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime updateTime;
更新文章
三层架构
ArticleController
@PutMapping public Result update (@RequestBody @Validated(Category.Update.class) Category category) { categoryService.update(category); return Result.success(); }
ArticleServiceImpl
@Override public void update (Category category) { category.setUpdateTime(LocalDateTime.now()); categoryMapper.update(category); }
ArticleMapper
@Update("update article set title=#{title},content=#{content},cover_img=#{coverImg},state=#{state},category_id=#{categoryId},update_time=now() where id=#{id}") void update (Article article) ;
参数校验
请求参数说明:
参数名称
说明
类型
是否必须
备注
id
主键ID
number
是
title
文章标题
string
是
content
文章正文
string
是
coverImg
封面图像地址
string
是
state
发布状态
string
是
已发布 | 草稿
categoryId
文章分类ID
number
是
分组校验:
public class Article { @NotNull(groups = Article.update.class) private Integer id; public interface add { } public interface update { } }
@PostMapping public Result add (@RequestBody @Validated(Article.add.class) Article article) {}
@PutMapping public Result update (@RequestBody @Validated(Article.update.class) Article article) {}
删除文章
三层架构
ArticleController
@DeleteMapping public Result delete (Integer id) { articleService.delete(id); return Result.success(); }
ArticleServiceImpl
@Override public void delete (Integer id) { articleMapper.delete(id); }
ArticleMapper
@Delete("delete from article where id=#{id}") void delete (Integer id) ;
文件上传
本地存储
FileUploadController
package com.zjf.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 ("D:\\桌面\\大事件\\files\\" + filename)); return Result.success("url地址......" ); } }
阿里云 OSS
阿里云对象存储 OSS ( Object Storage Service ),是一款海量、安全、低成本、高可靠的云存储服务。使用 OSS ,您
可以通过网络随时存储和调用包括文本、图片、音频和视频等在内的各种文件。
实现步骤
引入SDK
<dependency > <groupId > com.aliyun.oss</groupId > <artifactId > aliyun-sdk-oss</artifactId > <version > 3.15.1</version > </dependency >
导入 AliOssUtil工具类
package com.zjf.bigeventadmin.utils;import com.aliyun.oss.ClientException;import com.aliyun.oss.OSS;import com.aliyun.oss.OSSClientBuilder;import com.aliyun.oss.OSSException;import java.io.InputStream;public class AliOssUtil { private static final String ENDPOINT = "https://oss-cn-beijing.aliyuncs.com" ; private static final String ACCESS_KEY_ID = "LTAI5tQ8e13igWZUMTjMEEQV" ; private static final String SECRET_ACCESS_KEY = "MffMJoM24sc59SEBEJQDb0cfBVOAC9" ; private static final String BUCKET_NAME = "big-event-gwd" ; public static String uploadFile (String objectName, InputStream inputStream) { OSS ossClient = new OSSClientBuilder ().build(ENDPOINT,ACCESS_KEY_ID,SECRET_ACCESS_KEY); String url = "" ; try { ossClient.createBucket(BUCKET_NAME); ossClient.putObject(BUCKET_NAME, objectName, inputStream); 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; } }
注意 : ENDPOINT ,ACCESS_KEY_ID,ACCESS_KEY_SECRET,BUCKET_NAME根据个人对象存储OSS进行修改
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); }
登陆优化-redis
在原来的登陆实现中,用户登陆系统,当修改密码成功后,那将来用户需要使用新的密码重新登陆系统。重新登录成功后,后台会下发新的令牌,但旧的令牌在之前的程序中没有作废,仍然可以使用旧的令牌访问用户资源。
SpringBoot 集成 redis
导入 spring-boot-starter-data-redis 起步依赖
<dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-data-redis</artifactId > </dependency >
在 yml 配置文件中 , 配置 redis 连接信息
spring: data: redis: host: localhost port: 6379
调用 API(StringRedisTemplate) 完成字符串的存取操作
编写redis测试代码
package com.zjf;import org.junit.jupiter.api.Test;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.context.SpringBootTest;import org.springframework.data.redis.core.StringRedisTemplate;import org.springframework.data.redis.core.ValueOperations;import java.util.concurrent.TimeUnit;@SpringBootTest public class RedisTest { @Autowired private StringRedisTemplate stringRedisTemplate; @Test public void test1 () { ValueOperations<String, String> operations = stringRedisTemplate.opsForValue(); operations.set("username" , "zjf" ); operations.set("id" ,"1" ,15 , TimeUnit.SECONDS); } @Test public void test2 () { String username = stringRedisTemplate.opsForValue().get("username" ); System.out.println(username); } }
令牌主动失效机制
登录成功后,给浏览器响应令牌的同时,把该令牌存储到 redis 中
/省略无关代码,保持不变/ @Autowired private StringRedisTemplate stringRedisTemplate; @PostMapping("/login") public Result 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("用户名错误" ); } 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); ValueOperations<String, String> operations = stringRedisTemplate.opsForValue(); operations.set(token, token, 1 , TimeUnit.HOURS); ValueOperations<String, String> operations = stringRedisTemplate.opsForValue(); operations.set(token, token, 1 , 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" ); try { ValueOperations<String, String> operations = stringRedisTemplate.opsForValue(); String redisToken = operations.get(token); if (redisToken == null ) { throw new RuntimeException (); } Map<String, Object> claims = JwtUtil.parseToken(token); ThreadLocalUtil.set(claims); return true ; } catch (Exception e) { response.setStatus(401 ); return false ; } }
当用户修改密码成功后,删除 redis 中存储的旧令牌
@PatchMapping("/updatePwd") public Result updatePwd (@RequestBody Map<String,String> params,@RequestHeader("Authorization") String token) { String oldPwd = params.get("old_pwd" ); String newPwd = params.get("new_pwd" ); String re_pwd = params.get("re_pwd" ); if (!StringUtils.hasLength(oldPwd)||!StringUtils.hasLength(newPwd)||!StringUtils.hasLength(re_pwd)){ return Result.error("参数错误" ); } Map<String,Object> map= ThreadLocalUtil.get(); String username = (String) map.get("username" ); User loginUser = userService.findByUserName(username); if (!Md5Util.getMD5String(oldPwd).equals(loginUser.getPassword())){ return Result.error("旧密码错误" ); } if (Md5Util.getMD5String(newPwd).equals(loginUser.getPassword())){ return Result.error("新密码不能与旧密码一致" ); } if (!newPwd.equals(re_pwd)){ return Result.error("两次密码不一致" ); } userService.updatePwd(newPwd); ValueOperations<String, String> operations = stringRedisTemplate.opsForValue(); operations.getOperations().delete(token); return Result.success(); }
SpringBoot 项目部署
引入插件
<plugin > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-maven-plugin</artifactId > </plugin >
Maven中Lifecycle执行package命令
在target目录下,CMD执行命令java -jar big_event-0.0.1-SNAPSHOT.jar
属性配置方式
项目配置文件方式
在项目resources文件夹下application.yml文件下配置
命令行参数方式
java -jar big_event-0.0.1-SNAPSHOT.jar --server.port=8081
环境变量方式
外部配置文件方式
配置优先级
命令行参数
操作系统环境变量
Jar 包所在目录下的 application.yml
项目中 resources 目录下的 application.yml
多环境开发-Profiles
1. 单文件配置
在实际项目的开发过程中,我们程序往往需要在不同环境中运行。例如:开发环境、测试环境和生产环境。
每个环境中的配置参数可能都会有所不同,例如数据库连接信息、文件服务器等等。
SpringBoot 提供的 Profiles 可以用来隔离应用程序配置的各个部分,并在特定环境下指定部分配置生效
如何分隔不同环境的配置?
如何指定哪些配置属于哪个环境?
spring: config: activate: on-profile: 环境名称
开发(dev
)、测试(test
)、生产(prod
)分别建立配置文件
application.yml
或者application.properties
用于存放所有环境通用的配置,指定激活的环境
application-dev.yml
或者application-dev.properties
存放开发环境的特殊配置
application-test.yml
或者application-test.properties
存放测试环境的特殊配置
application-prod.yml
或者application-prod.properties
存放生产环境的特殊配置
多环境开发 -Pofiles- 分组
示例:
spring: profiles: active: dev group: "dev": devDB,devServer,devSelf "pro": proDB,proServer,proSelf "test": testDB,testServer,testself