项目经验


项目经验

自定义返回状态码

  • 设计一个接口,定义状态码和信息的获取方式
/**
 * 基础状态码接口
 */
public interface BaseCode {
    /**
     * 获取状态码
     * @return
     */
    Integer getCode();

    /**
     * 获取返回消息
     * @return
     */
    String getMessage();
}
  • 设计枚举类,实现上述接口,定义具体的异常信息和状态码
 public enum ExcepCode implements BaseCode {
    SUCCESS(1, "SUCCESS"),
    FAIL(2, "FAIL"),

    PHONE_FORMAT(3, "手机号码格式错误!"),
    ;

    public Integer code;

    public String message;

    ExcepCode(Integer code, String message) {
        this.code = code;
        this.message = message;
    }

    @Override
    public Integer getCode() {
        return code;
    }

    @Override
    public String getMessage() {
        return message;
    }
}

SpringBoot 配置 Redis 使用 FastJson 进行序列化

  • 引入架包
<!--redis-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<!--redis的lettuce连接池-->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>

<!--fastjson-->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.54</version>
</dependency>
  • application.yml 配置连接
spring: 
  redis:
    host: 127.0.0.1
    password:
    port: 6379
    database: 0
    # 连接超时时间(毫秒)
    timeout: 10s
    lettuce:
      # 关闭超时时间
      shutdown-timeout: 1s
      pool:
        # 连接池最大连接数(使用负值表示没有限制) 默认 8
        max-active: 1000
        # 连接池中的最大空闲连接 默认 8
        max-idle: 300
        # 连接池中的最小空闲连接 默认 0
        min-idle: 3
        # 连接池最大阻塞等待时间(使用负值表示没有限制) 默认 -1
        max-wait: 1s
  • 自定义 FastJsonRedisSerializer 用来实现 Redis 的序列化
/**
 * 自定义 FastJsonRedisSerializer 用来实现 Redis 的序列化
 * @param <T>
 */
public class FastJsonRedisSerializer<T> implements RedisSerializer<T> {

    public ObjectMapper objectMapper = new ObjectMapper();

    public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;

    private final Class<T> clazz;

    public FastJsonRedisSerializer(Class<T> clazz) {
        super();
        this.clazz = clazz;
    }

    @Override
    public byte[] serialize(T t) throws SerializationException {
        if (t == null) {
            return new byte[0];
        }
        return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
    }

    @Transactional
    @Override
    public T deserialize(byte[] bytes) throws SerializationException {
        if (bytes == null || bytes.length <= 0) {
            return null;
        }
        String str = new String(bytes, DEFAULT_CHARSET);
        return (T) JSON.parseObject(str, clazz);
    }

    public void setObjectMapper(ObjectMapper objectMapper) {
        Assert.notNull(objectMapper, "'objectMapper' must not be null");
        this.objectMapper = objectMapper;
    }

}
  • 新增 RedisConfig 配置类,配置 Redis 信息
/**
 * 配置 Redis,通过自定义的 FastJsonRedisSerializer 用来实现 Redis 的序列化
 * 如果这个类上加上 @EnableCaching 这个注解,然后重写 RedisCacheManager 这个 Bean,那么在 Service 层可以使用 @Cacheable、@CachePut 和 @CacheEvict 这些注解把对应的缓存数据写入 Redis
 */
@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<Object, Object> redisTemplate( @Lazy LettuceConnectionFactory connectionFactory) {
        RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());

        FastJsonRedisSerializer<Object> fastJsonRedisSerializer = new FastJsonRedisSerializer<>(Object.class);
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
        fastJsonRedisSerializer.setObjectMapper(objectMapper);
        redisTemplate.setHashValueSerializer(fastJsonRedisSerializer);
        redisTemplate.setValueSerializer(fastJsonRedisSerializer);

        redisTemplate.setConnectionFactory(connectionFactory);
        ParserConfig.getGlobalInstance().addAccept("com.rhx");
        return redisTemplate;
    }
}
  • 参考资料
  1. https://juejin.cn/post/6897914682238550023
  2. https://juejin.cn/post/7076244567569203208#heading-6
  3. https://blog.csdn.net/moshowgame/article/details/83246363

使用 PageHelper 进行分页

  • 引入架包
<!--使用pagehelper进行分页 -->
<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper-spring-boot-starter</artifactId>
    <version>1.4.6</version>
    <exclusions>
        <!--因为 mybatisplus 已经包含 mybatis了,这里去除,避免依赖冲突 -->
        <exclusion>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
        </exclusion>
        <!--因为 mybatisplus 已经包含 mybatis-spring了,这里去除,避免依赖冲突 -->
        <exclusion>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis-spring</artifactId>
        </exclusion>
    </exclusions>
</dependency>
  • application.yml 配置文件
# pagehelper 配置
pagehelper:
  helper-dialect: mysql
  # 分页插件会自动检测当前的数据库链接
  auto-dialect: true
  # 分页合理化参数,当该参数设置为 true 时,pageNum<=0 时会查询第一页,pageNum>pages (超过总数时),会查询最后一页
  reasonable: true
  # 支持通过 Mapper 接口参数传递 page 参数
  support-methods-arguments: true
  # 当该参数设置为 true 时,如果 pageSize=0 或者 RowBounds.limit =0 就会查询出全部的结果(相当于没有执行分页查询,但是返回结果仍然是 Page 类型)
  page-size-zero: true
  # 为了支持 startPage(Object params) 方法,增加了该参数来配置参数映射,用于从对象中根据属性名取值
  params: count=countSql
  • 代码编程
// service 层
public PageInfo<?> getSysUserPage(SysUserPageDTO pageDTO) {
    PageHelper.startPage(pageDTO.getPageNo(), pageDTO.getPageSize());
    List<SysUserPageVO> page = sysUserService.getPage(pageDTO);
    return new PageInfo<>(page);
}

// mapper 层,注意这里变量一定要使用 @Param() 修饰
List<SysUserPageVO> getPage(@Param("pageDTO") SysUserPageDTO pageDTO);
# 注意这里的变量带前缀:pageDTO,这个前缀就是 mapper 里 @Param() 修饰的
<select id="getPage" resultType="com.rhx.manage.model.vo.SysUserPageVO">
    select a.* from t_sys_user a
        <if test="pageDTO.status != null">
            and a.status = #{pageDTO.status}
        </if>
    order by a.create_time desc
</select>

菜单权限设计和多租户系统

  1. https://zhuanlan.zhihu.com/p/296519030
  2. https://blog.csdn.net/dxflqm_pz/article/details/128838366
  3. https://www.cnblogs.com/niuben/p/11063777.html
  4. https://blog.csdn.net/CBGCampus/article/details/128663600
  5. https://blog.csdn.net/qq_42764468/article/details/126719689
  6. https://blog.csdn.net/qq_37377082/article/details/117459588

SpringBoot 集成 JWT 实现 Token 验证

  • 引入架包
<!-- JWT -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

定义常量类

  • 定义 Redis 常量类
/**
 * Redis 常量类
 */
public class RedisConstants {

    /**
     * 注册验证码 key 前缀
     */
    public static final String REGISTER_CODE_KEY = "register_codes:";

    /**
     * 注册验证码 key 过期时间
     */
    public static final long REGISTER_TIME = 60;

    /**
     * 登录验证码 key 前缀
     */
    public static final String LOGIN_CODE_KEY = "login_codes:";

    /**
     * 缓存有效期
     */
    public final static long EXPIRED_TIME = 8 * 60 * 60;

    /**
     * 权限缓存前缀
     */
    public final static String LOGIN_TOKEN_KEY = "login_tokens:";
}
  • 定义权限相关的常量
/**
 * 权限相关通用常量
 */
public class SecurityConstants {

    /**
     * 用户ID字段
     */
    public static final String USER_ID = "user_id";

    /**
     * 用户手机号码段
     */
    public static final String USER_PHONE = "phone";

    /**
     * 用户标识
     */
    public static final String USER_KEY = "user_key";

    /**
     * 用户名字段
     */
    public static final String USER_NAME = "NAME";

}
  • 定义 Token 常量
/**
 * Token 常量类
 */
public class TokenConstants {

    /**
     * 令牌自定义标识
     */
    public static final String AUTHENTICATION = "Authorization";

    /**
     * 令牌前缀
     */
    public static final String PREFIX = "Bearer ";

    /**
     * 令牌秘钥
     */
    public final static String SECRET = "zyxwvutsrqponmlkjihgfedcba";

}

定义工具类

  • 定义客户端工具类
/**
 * 客户端工具类
 */
public class ServletUtils {

    /**
     * 获取String参数
     */
    public static String getParameter(String name)
    {
        return getRequest().getParameter(name);
    }

    /**
     * 获取String参数
     */
    public static String getParameter(String name, String defaultValue)
    {
        return Convert.toStr(getRequest().getParameter(name), defaultValue);
    }

    /**
     * 获取Integer参数
     */
    public static Integer getParameterToInt(String name)
    {
        return Convert.toInt(getRequest().getParameter(name));
    }

    /**
     * 获取Integer参数
     */
    public static Integer getParameterToInt(String name, Integer defaultValue)
    {
        return Convert.toInt(getRequest().getParameter(name), defaultValue);
    }

    /**
     * 获取Boolean参数
     */
    public static Boolean getParameterToBool(String name)
    {
        return Convert.toBool(getRequest().getParameter(name));
    }

    /**
     * 获取Boolean参数
     */
    public static Boolean getParameterToBool(String name, Boolean defaultValue)
    {
        return Convert.toBool(getRequest().getParameter(name), defaultValue);
    }

    /**
     * 获得所有请求参数
     *
     * @param request 请求对象{@link ServletRequest}
     * @return Map
     */
    public static Map<String, String[]> getParams(ServletRequest request)
    {
        final Map<String, String[]> map = request.getParameterMap();
        return Collections.unmodifiableMap(map);
    }

    /**
     * 获得所有请求参数
     *
     * @param request 请求对象{@link ServletRequest}
     * @return Map
     */
    public static Map<String, String> getParamMap(ServletRequest request)
    {
        Map<String, String> params = new HashMap<>();
        for (Map.Entry<String, String[]> entry : getParams(request).entrySet())
        {
            params.put(entry.getKey(), StringUtils.join(entry.getValue(), ","));
        }
        return params;
    }

    /**
     * 获取request
     */
    public static HttpServletRequest getRequest()
    {
        try
        {
            return getRequestAttributes().getRequest();
        }
        catch (Exception e)
        {
            return null;
        }
    }

    /**
     * 获取response
     */
    public static HttpServletResponse getResponse()
    {
        try
        {
            return getRequestAttributes().getResponse();
        }
        catch (Exception e)
        {
            return null;
        }
    }

    /**
     * 获取session
     */
    public static HttpSession getSession()
    {
        return getRequest().getSession();
    }

    public static ServletRequestAttributes getRequestAttributes()
    {
        try
        {
            RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
            return (ServletRequestAttributes) attributes;
        }
        catch (Exception e)
        {
            return null;
        }
    }

    public static String getHeader(HttpServletRequest request, String name)
    {
        String value = request.getHeader(name);
        if (StringUtils.isEmpty(value))
        {
            return StringUtils.EMPTY;
        }
        return urlDecode(value);
    }

    public static Map<String, String> getHeaders(HttpServletRequest request)
    {
        Map<String, String> map = new LinkedCaseInsensitiveMap<>();
        Enumeration<String> enumeration = request.getHeaderNames();
        if (enumeration != null)
        {
            while (enumeration.hasMoreElements())
            {
                String key = enumeration.nextElement();
                String value = request.getHeader(key);
                map.put(key, value);
            }
        }
        return map;
    }

    /**
     * 将字符串渲染到客户端
     * 
     * @param response 渲染对象
     * @param string 待渲染的字符串
     */
    public static void renderString(HttpServletResponse response, String string)
    {
        try
        {
            response.setStatus(200);
            response.setContentType("application/json");
            response.setCharacterEncoding("utf-8");
            response.getWriter().print(string);
        }
        catch (IOException e)
        {
            e.printStackTrace();
        }
    }

    /**
     * 是否是Ajax异步请求
     * 
     * @param request
     */
    public static boolean isAjaxRequest(HttpServletRequest request)
    {
        String accept = request.getHeader("accept");
        if (accept != null && accept.contains("application/json"))
        {
            return true;
        }

        String xRequestedWith = request.getHeader("X-Requested-With");
        if (xRequestedWith != null && xRequestedWith.contains("XMLHttpRequest"))
        {
            return true;
        }

        String uri = request.getRequestURI();
        if (inStringIgnoreCase(uri, ".json", ".xml"))
        {
            return true;
        }

        String ajax = request.getParameter("__ajax");
        return inStringIgnoreCase(ajax, "json", "xml");
    }

    /**
     * 是否包含字符串
     *
     * @param str 验证字符串
     * @param strs 字符串组
     * @return 包含返回true
     */
    public static boolean inStringIgnoreCase(String str, String... strs)
    {
        if (str != null && strs != null)
        {
            for (String s : strs)
            {
                if (str.equalsIgnoreCase(trim(s)))
                {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * 去空格
     * @param str
     * @return
     */
    public static String trim(String str)
    {
        return (str == null ? "" : str.trim());
    }

    /**
     * 内容编码
     * 
     * @param str 内容
     * @return 编码后的内容
     */
    public static String urlEncode(String str)
    {
        try
        {
            return URLEncoder.encode(str, "UTF-8");
        }
        catch (UnsupportedEncodingException e)
        {
            return StringUtils.EMPTY;
        }
    }

    /**
     * 内容解码
     * 
     * @param str 内容
     * @return 解码后的内容
     */
    public static String urlDecode(String str)
    {
        try
        {
            return URLDecoder.decode(str, "UTF-8");
        }
        catch (UnsupportedEncodingException e)
        {
            return StringUtils.EMPTY;
        }
    }

    /**
     * 设置webflux模型响应
     *
     * @param response ServerHttpResponse
     * @param value 响应内容
     * @return Mono<Void>
     */
    public static Mono<Void> webFluxResponseWriter(ServerHttpResponse response, Object value)
    {
        return webFluxResponseWriter(response, HttpStatus.OK, value, 500);
    }

    /**
     * 设置webflux模型响应
     *
     * @param response ServerHttpResponse
     * @param code 响应状态码
     * @param value 响应内容
     * @return Mono<Void>
     */
    public static Mono<Void> webFluxResponseWriter(ServerHttpResponse response, Object value, int code)
    {
        return webFluxResponseWriter(response, HttpStatus.OK, value, code);
    }

    /**
     * 设置webflux模型响应
     *
     * @param response ServerHttpResponse
     * @param status http状态码
     * @param code 响应状态码
     * @param value 响应内容
     * @return Mono<Void>
     */
    public static Mono<Void> webFluxResponseWriter(ServerHttpResponse response, HttpStatus status, Object value, int code)
    {
        return webFluxResponseWriter(response, MediaType.APPLICATION_JSON_VALUE, status, value, code);
    }

    /**
     * 设置webflux模型响应
     *
     * @param response ServerHttpResponse
     * @param contentType content-type
     * @param status http状态码
     * @param code 响应状态码
     * @param value 响应内容
     * @return Mono<Void>
     */
    public static Mono<Void> webFluxResponseWriter(ServerHttpResponse response, String contentType, HttpStatus status, Object value, int code)
    {
        response.setStatusCode(status);
        response.getHeaders().add(HttpHeaders.CONTENT_TYPE, contentType);
        DataBuffer dataBuffer = response.bufferFactory().wrap(value.toString().getBytes());
        return response.writeWith(Mono.just(dataBuffer));
    }
}
  • 定义 Token 相关的工具类
/**
 * SecurityUtils 工具类
 *
 */
public class SecurityUtils {

    /**
     * 获取请求token
     */
    public static String getToken()
    {
        return getToken(Objects.requireNonNull(ServletUtils.getRequest()));
    }

    /**
     * 根据request获取请求token
     */
    public static String getToken(HttpServletRequest request) {

        // 从 Hearder 中获取
        String header = request.getHeader(TokenConstants.AUTHENTICATION);

        // 从 Parameter 中获取
        if (StringUtils.isBlank(header)) {
            header = request.getParameter(TokenConstants.AUTHENTICATION);
        }

        // 从 Cookie 中获取
        if (StringUtils.isBlank(header)) {
            Cookie[] cookies = request.getCookies();
            if (ObjectUtils.isEmpty(cookies)) {
                throw new RuntimeException("Request Token Is Empty");
            }
            for (Cookie cookie : cookies) {
                if (Objects.equals(cookie.getName(), TokenConstants.AUTHENTICATION)) {
                    header = cookie.getValue();
                }
            }
        }

        // 都为空的情况
        if (StringUtils.isBlank(header)) {
            throw new RuntimeException("Request Token Is Empty");
        }

        if (!header.startsWith(TokenConstants.PREFIX)) {
            throw new RuntimeException("Request Token Bearer Is Empty");
        }

        return JwtUtils.getUserKey(header.substring(TokenConstants.PREFIX.length()));
    }
}
  • 定义 JWT 工具类
/**
 * Jwt工具类
 */
public class JwtUtils {

    public static String secret = TokenConstants.SECRET;

    /**
     * 从数据声明生成令牌
     *
     * @param claims 数据声明
     * @return 令牌
     */
    public static String createToken(Map<String, Object> claims)
    {
        return Jwts.builder().setClaims(claims).signWith(SignatureAlgorithm.HS512, secret).compact();
    }

    /**
     * 从令牌中获取数据声明
     *
     * @param token 令牌
     * @return 数据声明
     */
    public static Claims parseToken(String token)
    {
        return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
    }

    /**
     * 根据令牌获取用户标识
     * 
     * @param token 令牌
     * @return 用户ID
     */
    public static String getUserKey(String token)
    {
        Claims claims = parseToken(token);
        return getValue(claims, SecurityConstants.USER_KEY);
    }

    /**
     * 根据令牌获取用户标识
     * 
     * @param claims 身份信息
     * @return 用户ID
     */
    public static String getUserKey(Claims claims)
    {
        return getValue(claims, SecurityConstants.USER_KEY);
    }

    /**
     * 根据令牌获取用户ID
     * 
     * @param token 令牌
     * @return 用户ID
     */
    public static String getUserId(String token)
    {
        Claims claims = parseToken(token);
        return getValue(claims, SecurityConstants.USER_ID);
    }

    /**
     * 根据身份信息获取用户ID
     * 
     * @param claims 身份信息
     * @return 用户ID
     */
    public static String getUserId(Claims claims)
    {
        return getValue(claims, SecurityConstants.USER_ID);
    }

    /**
     * 根据令牌获取用户名
     * 
     * @param token 令牌
     * @return 用户名
     */
    public static String getUserName(String token)
    {
        Claims claims = parseToken(token);
        return getValue(claims, SecurityConstants.USER_PHONE);
    }

    /**
     * 根据身份信息获取用户名
     * 
     * @param claims 身份信息
     * @return 用户名
     */
    public static String getUserName(Claims claims)
    {
        return getValue(claims, SecurityConstants.USER_PHONE);
    }

    /**
     * 根据身份信息获取键值
     * 
     * @param claims 身份信息
     * @param key 键
     * @return 值
     */
    public static String getValue(Claims claims, String key)
    {
        return Convert.toStr(claims.get(key), "");
    }
}
  • 定义登录用户信息实体类
**
 * User 类,提供给生成 Token 和登录拦截使用
 */
@Data
public class LoginUser {

    /**
     * 用户ID
     */
    private long id;

    /**
     * 手机号码
     */
    private String phone;

    /**
     * 邮箱
     */
    private String email;

    /**
     * 姓名
     */
    private String name;

    /**
     * Token
     */
    private String token;

    /**
     * 最后登录时间,这里保存的是毫秒
     */
    private Long lastLoginTime;

    /**
     * Token 过期时间,这里保存的是毫秒
     */
    private Long tokenTime;

}
  • 定义记录登录用户信息工具类
/**
 * LoginUserUtils 工具类
 * 使用 ThreadLocal 存储用户登录信息
 * ThreadLocal 可以将用户信息保存在线程中,当请求结束后我们在把保存的信息清除掉。这样我们才开发的时候就可以直接从全局的 ThreadLocal中 很方便的获取用户信息。
 */
public class LoginUserUtils {

    private LoginUserUtils() {}

    private static final ThreadLocal<LoginUser> CURRENT_USER = new ThreadLocal<>();

    public static void set(LoginUser loginUser) {
        CURRENT_USER.set(loginUser);
    }

    public static LoginUser get() {
        return CURRENT_USER.get();
    }

    public static void remove() {
        CURRENT_USER.remove();
    }
}

代码实现

  • 用户登录
public R<?> login(SysUserLoginDTO dto) {

    // 验证姓名和密码
    TSysUser tSysUser = sysUserService.getOne(new LambdaQueryWrapper<TSysUser>().eq(TSysUser::getName, dto.getName()));

    ...

    // 保存用户登录日志 生成 Token 信息
    LoginUser loginUser = new LoginUser();
    loginUser.setId(tSysUser.getId());
    loginUser.setName(tSysUser.getName());
    loginUser.setEmail(tSysUser.getEmail());
    return R.ok(sysUserService.createToken(loginUser));
}
  • 创建 Token
    @Override
    public Map<String, Object> createToken(LoginUser loginUser) {
        //生成 Token
        loginUser.setToken(IdUtil.fastSimpleUUID().toUpperCase());
        this.refreshToken(loginUser);

        // Jwt存储信息
        Map<String, Object> claimsMap = new HashMap<String, Object>();
        claimsMap.put(SecurityConstants.USER_KEY, loginUser.getToken());
        claimsMap.put(SecurityConstants.USER_ID, loginUser.getId());
        claimsMap.put(SecurityConstants.USER_NAME, loginUser.getName());

        // 接口返回信息
        Map<String, Object> rspMap = new HashMap<String, Object>();
        rspMap.put("access_token", JwtUtils.createToken(claimsMap));
        rspMap.put("expired_time", RedisConstants.EXPIRED_TIME);
        return rspMap;
    }

    /**
     * 刷新令牌有效期
     * @param loginUser
     */
    @Override
    public void refreshToken(LoginUser loginUser) {

        // 保存用户信息到缓存
        loginUser.setLastLoginTime(System.currentTimeMillis());
        loginUser.setTokenTime(loginUser.getLastLoginTime() + RedisConstants.EXPIRED_TIME * 1000);
        String key = RedisConstants.LOGIN_TOKEN_KEY + loginUser.getToken();
        redisUtils.set(key, loginUser, RedisConstants.EXPIRED_TIME);

        // 更新数据库内的用户信息
        TSysUser tSysUser = new TSysUser();
        BeanUtils.copyProperties(loginUser, tSysUser);
        tSysUser.setUpdateBy(loginUser.getName());
        this.updateById(tSysUser);
    }
  • 定义接口拦截器
@Component
public class LoginInterceptor implements HandlerInterceptor {

    @Resource
    private RedisUtils redisUtils;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {

        // 判断 Token 是否已经失效,如果没有,重新延长,如果失效,重新登录
        // 先从缓存中获取数据
        String token = SecurityUtils.getToken(request);
        String key = RedisConstants.LOGIN_TOKEN_KEY + token;
        LoginUser loginUser = (LoginUser) redisUtils.get(key);
        if (ObjectUtils.isNotEmpty(loginUser)) {
            LoginUserUtils.set(loginUser);
            return true;
        } else {
            throw new RuntimeException("User Token Is Expires");
        }
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
        LoginUserUtils.remove();
    }
}
  • 启用拦截器
/**
 * 启用拦截器
 */
@Configuration
public class LoginConfig implements WebMvcConfigurer {

    @Resource
    private LoginInterceptor loginInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginInterceptor).addPathPatterns("/api/**")
                .excludePathPatterns("/api/user/login");
    }
}

Lombok 注解

@EqualsAndHashCode

@Data相当于@Getter @Setter @RequiredArgsConstructor @ToString @EqualsAndHashCode这5个注解的合集。

当使用@Data注解时,则有了@EqualsAndHashCode注解,那么就会在此类中存在equals(Object other)hashCode()方法,且不会使用父类的属性,这样当继承相同父类的子类去进行比较的时候,会存相等的情况。

解决方式

  1. 使用@Getter @Setter @ToString代替@Data并且自定义equals(Object other)hashCode()方法。
  2. 在使用@Data时同时加上@EqualsAndHashCode(callSuper=true)注解。

参考资料

https://blog.csdn.net/weixin_44536553/article/details/118180163

@Data

@Data相当于@Getter @Setter @RequiredArgsConstructor @ToString @EqualsAndHashCode这5个注解的合集。

@AllArgsConstructor

使用后添加一个构造函数,该构造函数含有所有已声明字段属性参数。

@NoArgsConstructor

使用后创建一个无参构造函数。

< 只添加 @Data 注解时,只有无参构造方法;添加 @AllArgsConstructor 和 @NoArgsConstructor 两个,才能同时有无参和带参构造方法。>

@Builder

作用之一是为了解决在某个类有很多构造函数的情况,也省去写很多构造函数的麻烦,在设计模式中的思想是:用一个内部类去实例化一个对象,避免一个类出现过多构造函数。

// 定义类 test,并添加 @Builder 注解
@Builder
public class test {
    String name;
    String age;
    String sex;
}

//使用@Builder注解后,可以直接通过Builder设置字段参数
test t = new test.testBuilder()
    .name("wang")
    .age("12")
    .sex("man")
    .build();

其它注解

@DateTimeFormat

该注解主要解决前端时间控件传值到后台接收准确的 Date 类属性的问题。一般使用为:@DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss")

@JsonFormat

该注解主要解决后台从数据库中取出时间类型赋予 JAVA 对象的 Date 属性值无法在前端以一定的日期格式来呈现。一般使用为:@JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss")

@NotNull

适用于基本数据类型 (Integer,Long,Double),当在 String 类型的数据上,则表示该数据不能为 Null(但是可以为 Empty)。

@NotBlank

适用于 String 类型的数据上,加了@NotBlank 注解的参数不能为 Null 且 trim() 之后 size > 0,必须有实际字符。

@NotEmpty

适用于 String、Collection集合、Map、数组。加了@NotEmpty 注解的参数不能为 Null 或者 长度为 0。

以上这些校验的注解都是需要 @Valid 或 @Validated 配合上使用才会生效。

  • @Valid 进行校验的时候,需要用 BindingResult 来做一个校验结果接收。当校验不通过的时候,如果手动不 return ,则并不会阻止程序的执行;
  • @Validated 进行校验的时候,当校验不通过的时候,程序会抛出400异常,阻止方法中的代码执行,这时需要再写一个全局校验异常捕获处理类,然后返回校验提示。

项目配置

指定目录下的 mapper.xml 文件不被扫描

  • pom.xml 文件下配置
<build>
    <!-- 配置加载配置项信息-->
    <resources>
        <resource>
            <directory>src/main/java</directory>
            <filtering>true</filtering>
            <includes>
                <include>**/*.xml</include>
                <include>**/*.json</include>
                <include>**/*.ftl</include>
            </includes>
        </resource>
    </resources>
</build>
  • application.yml 文件配置
# mybatis-plus 配置 
mybatis-plus:
# 指定加载文件路径
  mapper-locations: classpath*:com/rhx/manage/mapper/xml/*Mapper.xml

项目相互引用启动报错

如果 rhx-client 引用 rhx-api 项目里的方法,而 rhx-api 项目里有类加了 @Component 一类的 Spring 注解,那么在启动 rhx-client 项目的时候,需要在 rhx-client 的启动类上使用 @ComponentScan 指定 rhx-api 中加了注解类的路径。(rhx-api 项目不启动)

如果这样,那么 rhx-client 内加了 @Component 一类的 Spring 注解的类,在项目启动后,其类不会被 Spring 管理,也就是说 rhx-client 内需要 Spring 管理的类失效了。

解决办法:

  1. 在 rhx-client 的启动类上 @ComponentScan 内添加 rhx-client 内的注解类路径,之前的保留。
  2. 把 rhx-api 内添加了 @Component 一类的 Spring 注解的类,移到 rhx-client 内管理。

SpringBoot 多模块打包方式

https://blog.csdn.net/qq_42875345/article/details/110790933

  1. 父工程不需要打包生成指定文件,因此可以去除如下引用:
<plugins>
    <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
    </plugin>
</plugins>
  1. 项目 rhx-client 引用了项目 rhx-api,打包的时候想把 rhx-api 也包含进 rhx-client 打包生成的文件内,只需要在 rhx-client 项目的 pom.xml 文件内添加如下配置即可。
<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <executions>
                <execution>
                    <goals>
                        <goal>repackage</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>
  1. 一个项目,对应不同环境存在不同的配置文件,打包的时候就有两种方式。一种是只生成指定环境的配置文件,一种是所有环境的配置文件都包含。这个控制在如下配置:
<build>
    <!-- 配置加载配置项信息-->
    <resources>
        <resource>
            <directory>src/main/resources</directory>
            <filtering>true</filtering>
            <!-- 注释掉这一块,那么项目打包的时候对应的 dev, test, prod 这些配置文件都会生成 -->
            <!-- 不注释掉这一块,那么项目打包的时候只会生成选择的配置文件,如果不选择,那么打的包将没有配置文件 -->
            <!--  <includes>
                       <include>application.yml</include>
                       <include>application-${profile.name}.yml</include>
                       <include>**/*.xml</include>
                   </includes>-->
        </resource>
    </resources>
</build>

OpenFeign 接口的使用

多个 Feign 接口使用 @FeignClient 注解调用同一个名称的微服务时,会报异常。

解决方式

  1. 将所有 Feign 接口合并。
  2. 在配置文件内增加配置:spring.main.allow-bean-definition-overriding=true。
  3. 在 @FeignClient 注解上增加 contextId 属性,确保每个 Feign Client 的 contextId 唯一。

SpringBoot 调用外部接口

  1. 使用原始 HttpClient 请求。
  2. 使用 RestTemplate 方法。
  3. 使用 Feign 进行消费。

使用 Gateway 搭建项目,报 530 错误

pom.xml 加入下面架包

<!-- springcloud 的负载均衡,没有这个,网关会报 503-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>

<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
</dependency>

@Transactional 注解的使用

  • 在方法上使用 @Transactional 注解,事务起作用。
  • 在类上使用 @Transactional,对整个类的方法起作用。
  • 在方法上使用 @Transactional,但异常信息使用 tyr-catch 处理,事务不起作用。
  • 在同一个 service 内,@Transactional 注解作用在方法 A 上,方法 A 调方法 B,无论 A 和 B 内那个有异常,事务都起作用。
  • 在同一个 service 内,@Transactional 注解作用在方法 B 上,方法 A 调方法 B,无论 A 和 B 内那个有异常,事务都不起作用。
  • 在同一个 service 内,@Transactional 注解作用类上,方法 A 调方法 B,无论 A 和 B 内那个有异常,事务都起作用。
  • 在同一个 service 内,@Transactional 注解作用在方法 A 上,方法 A 调方法 B(方法 B 私有),无论 A 和 B 内那个有异常,事务都起作用。
  • 在同一个 service 内,@Transactional 注解作用在方法 B 上(方法 B 私有),方法 A 调方法 B(方法 B 私有),无论 A 和 B 内那个有异常,事务都不起作用。
  • 不同 service 内,@Transactional 注解作用在方法 A 上,方法 A 调用其它 service 内的方法 B,无论 A 和 B 内那个有异常,事务都起作用。
  • 不同 service 内,@Transactional 注解作用在方法 B 上,方法 A 调用其它 service 内的方法 B,方法 A 的事务不起作用,方法 B 的事务起作用。

参考资料

https://blog.csdn.net/lianjian6534/article/details/112434905

引入 SpringBootActuator 后 Swagger3 失效

  • 在 SwaggerConfig 配置类中加入下面 Bean
@Bean
public static BeanPostProcessor springfoxHandlerProviderBeanPostProcessor() {
    return new BeanPostProcessor() {
        @Override
        public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
            if (bean instanceof WebMvcRequestHandlerProvider || bean instanceof WebFluxRequestHandlerProvider) {
                customizeSpringfoxHandlerMappings(getHandlerMappings(bean));
            }
            return bean;
        }

        private <T extends RequestMappingInfoHandlerMapping> void customizeSpringfoxHandlerMappings(List<T> mappings) {
            List<T> copy = mappings.stream()
                    .filter(mapping -> mapping.getPatternParser() == null)
                    .collect(Collectors.toList());
            mappings.clear();
            mappings.addAll(copy);
        }

        @SuppressWarnings("unchecked")
        private List<RequestMappingInfoHandlerMapping> getHandlerMappings(Object bean) {
            try {
                Field field = ReflectionUtils.findField(bean.getClass(), "handlerMappings");
                field.setAccessible(true);
                return (List<RequestMappingInfoHandlerMapping>) field.get(bean);
            } catch (IllegalArgumentException | IllegalAccessException e) {
                throw new IllegalStateException(e);
            }
        }
    };
}

文章作者: L Q
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 L Q !
  目录