项目经验
自定义返回状态码
- 设计一个接口,定义状态码和信息的获取方式
/**
* 基础状态码接口
*/
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;
}
}
- 参考资料
使用 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>
菜单权限设计和多租户系统
- https://zhuanlan.zhihu.com/p/296519030
- https://blog.csdn.net/dxflqm_pz/article/details/128838366
- https://www.cnblogs.com/niuben/p/11063777.html
- https://blog.csdn.net/CBGCampus/article/details/128663600
- https://blog.csdn.net/qq_42764468/article/details/126719689
- 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()
方法,且不会使用父类的属性,这样当继承相同父类的子类去进行比较的时候,会存相等的情况。
解决方式
- 使用
@Getter @Setter @ToString
代替@Data
并且自定义equals(Object other)
和hashCode()
方法。 - 在使用
@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 管理的类失效了。
解决办法:
- 在 rhx-client 的启动类上 @ComponentScan 内添加 rhx-client 内的注解类路径,之前的保留。
- 把 rhx-api 内添加了 @Component 一类的 Spring 注解的类,移到 rhx-client 内管理。
SpringBoot 多模块打包方式
- 父工程不需要打包生成指定文件,因此可以去除如下引用:
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
- 项目 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>
- 一个项目,对应不同环境存在不同的配置文件,打包的时候就有两种方式。一种是只生成指定环境的配置文件,一种是所有环境的配置文件都包含。这个控制在如下配置:
<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 注解调用同一个名称的微服务时,会报异常。
解决方式
- 将所有 Feign 接口合并。
- 在配置文件内增加配置:spring.main.allow-bean-definition-overriding=true。
- 在 @FeignClient 注解上增加 contextId 属性,确保每个 Feign Client 的 contextId 唯一。
SpringBoot 调用外部接口
- 使用原始 HttpClient 请求。
- 使用 RestTemplate 方法。
- 使用 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);
}
}
};
}