1 pom 依赖
1.1 MyBatis Spring
用于简化 MyBatis 与 Spring Boot 的集成,提供了对 MyBatis 框架的自动配置支持,简化了数据访问层的开发
1.2 Lombok
Lombok 是一个 Java 库,能够通过注解自动生成常见的代码(如 getter
、setter
、toString
等),减少了代码冗余,提升了开发效率
1. @Data
- 作用: 这是一个综合性注解,包含了
@Getter
、@Setter
、@ToString
、@EqualsAndHashCode
和@RequiredArgsConstructor
,也就是说,@Data
会为所有字段自动生成getter
和setter
,同时生成toString()
、equals()
、hashCode()
方法,并且为final
字段生成构造函数。
2. @Builder
- 作用: 生成建造者模式(Builder Pattern)代码,可以通过流式调用来构建对象。它允许更灵活、清晰地创建对象,尤其适合包含多个属性的复杂对象创建。
3. @NoArgsConstructor
- 作用: 生成无参数的构造函数。适用于需要创建空对象的情况,比如从数据库反序列化时,或者某些框架(如 JPA)需要一个无参构造函数。
4. @AllArgsConstructor
- 作用: 生成包含所有字段的全参数构造函数。适用于在创建对象时直接通过构造函数初始化所有字段。
1.3 FastJSON
FastJSON 是阿里巴巴开源的一个高性能 JSON 序列化/反序列化库,主要用于将 Java 对象转换为 JSON 字符串或将 JSON 字符串转换为 Java 对象
1.4 Commons Lang
提供了一些常用的 Java 工具类,扩展了 Java 核心库中的 java.lang
包,提供了字符串操作、数字处理等实用功能
1.5 Druid
Druid 是阿里巴巴开源的高效数据库连接池,集成了数据库监控和性能优化功能。druid-spring-boot-starter
提供了与 Spring Boot 的自动集成
1.6 PageHelper
PageHelper 是一个 MyBatis 分页插件,简化了分页操作,支持多种数据库并能自动分页
1.7 Knife4j
Knife4j 是基于 Swagger 的增强 UI,提供了更丰富的文档功能,便于 API 文档的生成与查看
1.8 AspectJ
AspectJ 是一个面向切面编程(AOP)框架,aspectjrt
是其运行时库,用于在运行时处理切面相关的逻辑
AspectJjweaver
是用于支持 AspectJ 切面的织入器(weaver),允许在编译期、类加载期或运行期插入切面代码
1.9 JSON Web Token (JJWT)
JJWT 是用于生成和验证 JSON Web Tokens (JWT) 的库,主要用于处理身份验证和安全
1.10 Aliyun OSS
阿里云对象存储(OSS)SDK,用于与阿里云 OSS 进行集成,实现文件的上传、下载和管理
1.11 JAXB API
JAXB(Java Architecture for XML Binding)是用于将 Java 对象转换为 XML 以及将 XML 转换为 Java 对象的 API
2 项目结构
common:相当于 util,放置自定义的方法
pojo:Plain Old Java Object,放置对象
server:服务端代码
2.1 common
2.1.1 constant
常量类
如
public class JwtClaimsConstant {
public static final String EMP_ID = "empId";
public static final String USER_ID = "userId";
public static final String PHONE = "phone";
public static final String USERNAME = "username";
public static final String NAME = "name";
}
2.1.2 context
上下文
public class BaseContext {
public static ThreadLocal<Long> threadLocal = new ThreadLocal<>();
public static void setCurrentId(Long id) {
threadLocal.set(id);
}
public static Long getCurrentId() {
return threadLocal.get();
}
public static void removeCurrentId() {
threadLocal.remove();
}
}
管理当前线程中的上下文信息,特别是与线程相关的 Long
类型的 id
。通过使用 ThreadLocal
,它允许每个线程独立存储和访问自己的 id
,从而确保在多线程环境中不会出现线程间数据冲突的问题
这样可以 在任何时候获取用户 ID
2.1.3 exception
异常类
/**
* 登录失败
*/
public class LoginFailedException extends BaseException {
public LoginFailedException(String msg) {
super(msg);
}
}
2.1.4 json
将 Java 对象与 JSON 之间进行序列化和反序列化
通过这个类,可以将 Java
对象转换为 JSON
,或者将 JSON
转换为 Java
对象
/**
* 对象映射器:基于 jackson 将 Java 对象转为 json,或者将 json 转为 Java 对象
* 将 JSON 解析为 Java 对象的过程称为 [从 JSON 反序列化 Java 对象]
* 从 Java 对象生成 JSON 的过程称为 [序列化 Java 对象到 JSON]
*/
public class JacksonObjectMapper extends ObjectMapper {
public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
//public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm";
public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";
public JacksonObjectMapper() {
super();
//收到未知属性时不报异常
this.configure(FAIL_ON_UNKNOWN_PROPERTIES, false);
//反序列化时,属性不存在的兼容处理
this.getDeserializationConfig().withoutFeatures(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
SimpleModule simpleModule = new SimpleModule()
.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)))
.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));
//注册功能模块 例如,可以添加自定义序列化器和反序列化器
this.registerModule(simpleModule);
}
}
2.1.5 properties
属性类
@Component
@ConfigurationProperties(prefix = "sky.jwt") // 配置属性类
@Data
public class JwtProperties {
/**
* 管理端员工生成jwt令牌相关配置
*/
private String adminSecretKey;
private long adminTtl;
private String adminTokenName;
/**
* 用户端微信用户生成jwt令牌相关配置
*/
private String userSecretKey;
private long userTtl;
private String userTokenName;
}
这里有个 配置属性类 的注解 @ConfigurationProperties(prefix = "sky.jwt"),将配置文件 application.yml 中的配置属性 映射 到 Java 类,前缀 prefix 指定了配置属性:
# application.yml
sky:
jwt:
# 设置jwt签名加密时使用的秘钥
admin-secret-key: itcast
# 设置jwt过期时间
admin-ttl: 72000000000
# 设置前端传递过来的令牌名称
admin-token-name: token
# 设置jwt签名加密时使用的秘钥
user-secret-key: userKey
# 设置jwt过期时间
user-ttl: 72000000000
# 设置前端传递过来的令牌名称 与 前端 一致,不可更改
user-token-name: authentication
2.1.6 result
结果类
/**
* 封装分页查询结果
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PageResult implements Serializable {
private long total; //总记录数
private List records; //当前页数据集合
}
/**
* 后端统一返回结果
*
* @param <T>
*/
@Data
public class Result<T> implements Serializable {
private Integer code; //编码:1成功,0和其它数字为失败
private String msg; //错误信息
private T data; //数据
public static <T> Result<T> success() {
Result<T> result = new Result<T>();
result.code = 1;
return result;
}
public static <T> Result<T> success(T object) {
Result<T> result = new Result<T>();
result.data = object;
result.code = 1;
return result;
}
public static <T> Result<T> error(String msg) {
Result result = new Result();
result.msg = msg;
result.code = 0;
return result;
}
}
2.1.7 utils
实用类,包括AliOss对象文件上传,jwt令牌
@Data
@AllArgsConstructor
@Slf4j
public class AliOssUtil {
private String endpoint;
private String accessKeyId;
private String accessKeySecret;
private String bucketName;
/**
* 文件上传
*
* @param bytes
* @param objectName
* @return
*/
public String upload(byte[] bytes, String objectName) {
// 创建OSSClient实例。
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
try {
// 创建PutObject请求。
ossClient.putObject(bucketName, objectName, new ByteArrayInputStream(bytes));
} 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();
}
}
//文件访问路径规则 https://BucketName.Endpoint/ObjectName
StringBuilder stringBuilder = new StringBuilder("https://");
stringBuilder
.append(bucketName)
.append(".")
.append(endpoint)
.append("/")
.append(objectName);
log.info("文件上传到:{}", stringBuilder.toString());
return stringBuilder.toString();
}
}
public class JwtUtil {
/**
* 生成jwt
* 使用Hs256算法, 私匙使用固定秘钥
*
* @param secretKey jwt秘钥
* @param ttlMillis jwt过期时间(毫秒)
* @param claims 设置的信息
* @return
*/
public static String createJWT(String secretKey, long ttlMillis, Map<String, Object> claims) {
// 指定签名的时候使用的签名算法,也就是header那部分
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
// 生成JWT的时间
long expMillis = System.currentTimeMillis() + ttlMillis;
Date exp = new Date(expMillis);
// 设置jwt的body
JwtBuilder builder = Jwts.builder()
// 如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
.setClaims(claims)
// 设置签名使用的签名算法和签名使用的秘钥
.signWith(signatureAlgorithm, secretKey.getBytes(StandardCharsets.UTF_8))
// 设置过期时间
.setExpiration(exp);
return builder.compact();
}
/**
* Token解密
*
* @param secretKey jwt秘钥 此秘钥一定要保留好在服务端, 不能暴露出去, 否则sign就可以被伪造, 如果对接多个客户端建议改造成多个
* @param token 加密后的token
* @return
*/
public static Claims parseJWT(String secretKey, String token) {
// 得到DefaultJwtParser
Claims claims = Jwts.parser()
// 设置签名的秘钥
.setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8))
// 设置需要解析的jwt
.parseClaimsJws(token).getBody();
return claims;
}
}
2.2 pojo
分为
entity(实体对象)
vo(View Object,视图对象)
dto(Data Transfer Object,数据传输对象)
2.2.1 entity
- 以 Entity 结尾
- 数据对象名 与 数据库表名 一致
- 字段 与 数据库字段 一致
2.2.2 vo
用于 展示层,作用是把 某个指定页面(或组件)的所有数据封装
- 不可继承自 Entity
- vo 可以继承、组合其他 DTO、VO、BO 等对象
- vo 只能用于返回前端
2.2.3 dto
用于从数据库中检索数据
2.3 server
除了 controller、service、mapper 三个层,还有其他一些类
2.3.1 config
配置类
@Configuration
@Slf4j
public class RedisConfiguration {
@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
log.info("开始创建redis模板类");
// RedisTemplate redisTemplate = new RedisTemplate();
// redisTemplate.setConnectionFactory(redisConnectionFactory);
//
redisTemplate.setKeySerializer(new StringRedisSerializer());
//
// return redisTemplate;
RedisTemplate redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
// 设置Key的序列化器为String
redisTemplate.setKeySerializer(new StringRedisSerializer());
// 设置Value的序列化器为String
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
// // 设置Hash Key的序列化器为String
// redisTemplate.setHashKeySerializer(new StringRedisSerializer());
//
// // 设置Hash Value的序列化器为String
// redisTemplate.setHashValueSerializer(new StringRedisSerializer());
return redisTemplate;
}
}
redis 数据库
web 层配置
/**
* 配置类,注册web层相关组件
*/
@Configuration
@Slf4j
public class WebMvcConfiguration extends WebMvcConfigurationSupport {
@Autowired
private JwtTokenAdminInterceptor jwtTokenAdminInterceptor;
@Autowired
private JwtTokenUserInterceptor jwtTokenUserInterceptor;
/**
* 注册自定义拦截器
*
* @param registry
*/
protected void addInterceptors(InterceptorRegistry registry) {
log.info("开始注册自定义拦截器...");
registry.addInterceptor(jwtTokenAdminInterceptor)
.addPathPatterns("/admin/**") // 添加拦截路径
.excludePathPatterns("/admin/employee/login"); // 排除拦截路径(登录不需要拦截)
registry.addInterceptor(jwtTokenUserInterceptor)
.addPathPatterns("/user/**")
.excludePathPatterns("/user/user/login")
.excludePathPatterns("/user/shop/status");
}
/**
* 通过 knife4j 生成接口文档
*
* @return
*/
@Bean
public Docket docket1() {
ApiInfo apiInfo = new ApiInfoBuilder()
.title("苍穹外卖项目接口文档")
.version("2.0")
.description("苍穹外卖项目接口文档")
.build();
Docket docket = new Docket(DocumentationType.SWAGGER_2)
.groupName("管理端接口")
.apiInfo(apiInfo)
.select()
.apis(RequestHandlerSelectors.basePackage("com.sky.controller.admin"))
.paths(PathSelectors.any())
.build();
return docket;
}
@Bean
public Docket docket2() {
ApiInfo apiInfo = new ApiInfoBuilder()
.title("苍穹外卖项目接口文档")
.version("2.0")
.description("苍穹外卖项目接口文档")
.build();
Docket docket = new Docket(DocumentationType.SWAGGER_2)
.groupName("用户端接口")
.apiInfo(apiInfo)
.select()
.apis(RequestHandlerSelectors.basePackage("com.sky.controller.user"))
.paths(PathSelectors.any())
.build();
return docket;
}
/**
* 设置静态资源映射
*
* @param registry
*/
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/doc.html").addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
}
/**
* 扩展 Spring MVC 的消息转换器
*
* @param converters
*/
@Override
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
// 创建一个 消息转换器 对象
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
// 为 消息转换器 设置一个 对象转换器,对象转换器可以将 Java 对象序列化为 Json 数据
converter.setObjectMapper(new JacksonObjectMapper());
// 将自己的 消息转换器 加入到 容器 中,放置在第一位
converters.add(0, converter);
}
}
2.3.2 handler
@RestControllerAdvice 可以捕获并处理控制器层抛出的异常,从而统一管理异常处理逻辑,发生异常时,sping自动调用 handler 方法
/**
* 全局异常处理器,处理项目中抛出的业务异常
*/
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
/**
* 捕获业务异常
*
* @param ex
* @return
*/
@ExceptionHandler
public Result exceptionHandler(BaseException ex) {
log.error("异常信息:{}", ex.getMessage());
return Result.error(ex.getMessage());
}
/**
* 处理 SQL 异常
*
* @param ex
* @return
*/
@ExceptionHandler
public Result exceptionHandler(SQLIntegrityConstraintViolationException ex) {
// Duplicate entry 'ada' for key 'employee.idx_username'
if (ex.getMessage().contains("Duplicate entry")) {
String[] s = ex.getMessage().split(" ");
String username = s[2]; // 拿到用户名
String msg = username + MessageConstant.ALREADY_EXIST;
return Result.error(msg);
} else {
return Result.error(MessageConstant.UNKNOWN_ERROR);
}
}
}
2.3.3 interceptor
拦截器
/**
* jwt令牌校验的拦截器
*/
@Component
@Slf4j
public class JwtTokenAdminInterceptor implements HandlerInterceptor {
@Autowired
private JwtProperties jwtProperties;
/**
* 校验jwt
*
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//判断当前拦截到的是 Controller 的方法还是其他资源
if (!(handler instanceof HandlerMethod)) {
//当前拦截到的不是动态方法,直接放行
return true;
}
//1、从请求头中获取令牌
String token = request.getHeader(jwtProperties.getAdminTokenName());
//2、校验令牌
try {
log.info("jwt 校验:{}", token);
Claims claims = JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(), token);
Long empId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString());
log.info("当前员工id:", empId);
BaseContext.setCurrentId(empId);
//3、通过,放行
return true;
} catch (Exception ex) {
//4、不通过,响应 401 状态码
response.setStatus(401);
return false;
}
}
}
2.3.3 启动类
@SpringBootApplication
@EnableTransactionManagement //开启注解方式的事务管理
@Slf4j
@EnableCaching // 开启缓存注解
@EnableScheduling // 开启任务调度
public class SkyApplication {
public static void main(String[] args) {
SpringApplication.run(SkyApplication.class, args);
log.info("server started");
}
}
2.3.4 配置
服务器端口号
server:
port: 8080
dev:开发环境
spring:
profiles:
active: dev
main:
allow-circular-references: true
datasource:
druid:
driver-class-name: ${sky.datasource.driver-class-name}
url: jdbc:mysql://${sky.datasource.host}:${sky.datasource.port}/${sky.datasource.database}?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true
username: ${sky.datasource.username}
password: ${sky.datasource.password}
redis:
host: localhost
port: 6379
password: 123456
mybatis 配置
type-aliases-package: MyBatis 的类型别名包路径。MyBatis 提供了类型别名功能,可以为 Java 类型指定简短的别名,便于在 Mapper XML 文件中使用
开发环境下
数据库连接配置 和 阿里云oss