基础配置
常用依赖
-
Spring Web依赖自动配置一些基本的WEB环境,比如Tomcat服务器的集成,Spring MVC核心组件等
<!--Spring Boot Web--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
-
Lombok 对实体类的注解, 生成构造方法, get/set方法等
<!--LomBok--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency>
-
mysql 实现对数据库的操作
<!--MySQL 连接组件--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency>
-
mybatis 数据处理持久层框架, 连接数据库
<!--MyBaits--> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>3.0.3</version> </dependency>
application.yml
注意事项
- 严格按照格式书写, 各级之间使用 冒号加空格分割, 控制各级之间的缩进
- 可以和properties互换
spring:
application:
name: 模块或者项目名称
#数据库配置
datasource:
url: jdbc:mysql://localhost:3306/数据库名称
driver-class-name: com.mysql.cj.jdbc.Driver
username: 数据库用户名
password: 数据库密码
#文件上传大小限制
servlet:
multipart:
max-file-size: 单个文件的最大大小
max-request-size: 整个请求所有文件的最大大小
mybatis:
configuration:
#让mybatis每次运行sql代码的时候再控制台打印输出
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
#mybatis大小写驼峰转换
map-underscore-to-camel-case: true
#spring日志打印 打印'debug'及以上类型的日志
logging:
level:
org.springframework.jdbc.support.JdbcTransactionManager: debug
#阿里云OSS地址
aliyun:
oss:
endpoint: 阿里云端点
bucketName: 存储空间名称
#这两个一般不用写,一般配置在环境变量里面了。这两个是配置Access Key ID和Access Key Secret
accessKeyId: 从阿里云获取Access Key Id
accessKeySecret: 从阿里云获取Access Key secret
server:
port: 端口号
address: 地址
application.properties
spring.application.name=模块或项目名称
#数据库连接
spring.datasource.url=jdbc:mysql://localhost:3306/数据库名称
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.username=数据库用户名
spring.datasource.password=数据库密码
#让mybatis每次运行sql代码的时候再控制台打印输出
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
#让mybatis自动让两个规范的命名转换,小驼峰转下划线
mybatis.configuration.map-underscore-to-camel-case=true
#spring日志打印 打印'debug'及以上类型的日志
logging.level.org.springframework.jdbc.support.JdbcTransactionManager = debug
#阿里云OSS配置
aliyun.oss.endpoint=阿里云端点
aliyun.oss.bucketName=存储空间名称
#设置启动端口
server.port=端口号
SpringBoot常用注解
依赖注入
注解名称 | 注解位置 | 注解作用 |
---|---|---|
@Component | 类上 | 标识一个类是Spring容器的一个组件(Bean),可以被Spring容器自动检测和注册 |
@Autowired | 类、方法、字段、构造函数或局部变量上 | Spring提供的依赖注入注解 , 根据类型自动装配依赖的对象 有多个同一类型的bean可以注入时 , Spring会根据名称来进行匹配 |
@Resource | 类、方法、字段、构造函数或局部变量上 | 是 Java EE 中的注解,也用于依赖注入 默认按照名称进行装配,如果没有指定名称,则按照字段名或方法参数名去匹配 bean 的名称,如果找不到则按照类型进行匹配 |
@Qualifier | 类、方法、字段、构造函数或局部变量上 | 通常与 @Autowired 一起使用, 当有多个同一类型的 bean 时,通过 @Qualifier 指定要注入的 bean 的名称,来消除 @Autowired 按类型注入时的歧义。 |
@Primary | 类上 | 当有多个同一类型的 bean 时,被标注为 @Primary 的 bean 将作为默认的选择,在没有明确指定 @Qualifier 的情况下,@Autowired 会优先注入被标注为 @Primary 的 bean。 |
@Slf4j | 类上 | @Slf4j 用于自动为类添加一个基于 Slf4j 日志框架的日志对象(log),可以方便地进行日志记录 |
@Log | 类上 | @Log 是 JDK 自带的日志注解,使用起来相对简单,但功能不如 Slf4j 强大 |
@Value(“${要引入的值的位置}”) | 字段、方法或方法参数级别上 | 用于直接将配置值注入到 Java 类的字段中。它可以用于注入各种类型的值,如字符串、数字等。 |
@ConfigurationProperties | 类上 | 可以批量注入属性、验证等,并且可以更容易地与配置文件中的复杂属性结构相匹配。 |
@Value和@ConfigurationProperties使用举例:
public class Test {//这里的${property.name}是从Spring的PropertySources中获取的属性值,PropertySources多种来源,如application.properties文件,环境变量,命令行参数等。
@Value("${property.name}")
private String propertyName;
public void test(@Value("${property.name}") String name)
}
@ConfigurationProperties(prefix = "property")
public class Test{//从配置文件中读取property.name和property.age
private String name;
private int age;
}
Contorller控制层
注解名称 | 注解位置 | 注解作用 |
---|---|---|
@Controller | 类上 | 标识控制层组件 |
@RestController | 类上 | 实际为@ResponseBody + @Controller @ResponseBody : 将返回的对象自动序列化为JSON或XML |
@RequestMapping | 类上, 方法上 | 在类上用于定义基本的请求路径前缀 在方法上用于定义具体的HTTP请求方法和路径映射 |
@GetMapping | 类上, 方法上 | 映射GET请求,常用于查询 |
@PostMapping | 类上, 方法上 | 映射POST请求,常用于添加 , 修改 |
@PutMapping | 类上, 方法上 | 映射PUT请求,常用于修改 |
@DeleteMapping | 类上, 方法上 | 映射DELETE请求,常用于删除 |
@RequestParam | 参数上 | 用于在请求中获取参数的值 可以指定参数是否必须 , 以及设置默认值等 通过 HTTP 请求的查询参数(如 ?name=value)传递数据给后端时使用 |
@PathVariable | 参数上 | 用于从请求的URL路径上获取参数值 通常与RESTful风格的URL设计一起使用 |
@RequestBody | 参数上 | 用于将 HTTP 请求体中的数据(通常是 JSON 或 XML 格式)绑定到方法参数上 主要用于接受POST , PUT等请求中提交的数据实体 当客户端通过 HTTP 请求的请求体提交复杂的数据对象时 |
@ResponseBody | 类上,方 法上 | 标注在方法上时,将方法的返回值直接作为 HTTP 响应体的内容返回,通常是 JSON、XML 等数据格式 标注在类上时,该类中的所有方法的返回值都将被直接作为响应体内容 使用场景 : 构建 RESTful API 或者需要直接返回数据给客户端而不是跳转页面时 , 常与@Controller 组合成@RestController使用 |
Service服务层
注解名称 | 注解位置 | 注解作用 |
---|---|---|
@Service | 类上 | 标识服务层组件 |
@Transactionl | 类上或者方法上 | 表示方法和类需要开启事务,当作用与类上时,类中所有方法均会开启事务,当作用于方法上时,方法开启事务,方法上的注解无法被子类所继承。(建议该注解标注在具体的实现类或实现类的方法上) |
Dao数据访问层
注解名称 | 注解位置 | 注解作用 |
---|---|---|
@Respository | 类上 | 标识数据访问层(Data Access Layer,DAL) 组件 |
@Mapper | 方法上 | MyBatis环境下 , 标注在接口上 在整合 MyBatis 时,用于标识这是一个 MyBatis 的映射接口 |
@MapKey(“键名”) | 参数上 | 声明Map的键名,不写也行 |
@Param(“需要传递的参数名”) | 方法上 | 在MyBatis框架中,这个注解用于在SQL映射文件中的预编译语句中传递多个参数 |
@Options(useGeneratedKeys=true/false,keyProperty=“键名”) | 方法上 | 通常与 @Insert 等注解一起使用,标注在方法上 可以设置一些额外的选项,比如使用生成的键(如自增主键)等 |
@Result | 方法上 | 通常在 @Results 注解内部使用 用于定义数据库列与实体类属性之间的映射关系 |
@Results | 方法上 | 标注在接口中的方法上 可以将多个 @Result 注解封装在一起,形成一个整体的结果映射配置 |
//@Options用法示例
@Insert("INSERT INTO users(username, password) VALUES(#{username}, #{password})")
@Options(useGeneratedKeys = true, keyProperty = "id")
int insertUser(User user);
//Result和Results的用法示例:
@Results({
@Result(property = "id", column = "user_id"),
@Result(property = "username", column = "user_name")
})
@Select("SELECT user_id, user_name FROM users")
List<User> getUsers();
封装实体类
注解名称 | 注解位置 | 注解作用 |
---|---|---|
@Data | 类上 | 组合注解,同时添加了 @Getter、@Setter、@ToString、@EqualsAndHashCode 和 @RequiredArgsConstructor 注解 可以自动生成类的 getter、setter、toString、equals 和 hashCode 等方法 |
@Getter / @Setter | 类上 | @Getter 用于为类的属性自动生成 getter 方法,@Setter 用于生成 setter 方法 |
@NoArgsConstructor | 类上 | 为类生成一个无参的构造函数 |
@AllArgsConstructor | 类上 | 为类生成一个包含所有属性的有参构造函数 |
@DataTimeFormat(pattern = “”) | 参数、字段上 | 用于将外部输入的字符串格式的日期时间数据,按照指定的格式转换为 Java 中的日期时间对象 |
全局异常处理
注解名称 | 注解位置 | 注解作用 |
---|---|---|
@RestControllerAdvice | 类上 | 是一个组合注解,它结合了 @ControllerAdvice 和 @ResponseBody 的功能 用于定义全局的异常处理类,能够捕获控制器中抛出的异常,并进行统一的处理。 可以通过定义多个方法,每个方法处理一种特定类型的异常 |
@ExceptionHandler | 方法上 | 用于在 @ControllerAdvice 或 @RestControllerAdvice 标注的类中定义异常处理方法 可以指定一个或多个异常类型作为参数,当控制器中抛出这些类型的异常时,对应的异常处理方法会被调用 |
//全局异常处理举例
@ControllerAdvice //可以直接使用 @RestControllerAdvice , 需要删除下面的 @ResponseBody
public class GlobalExceptionHandler {
@ResponseBody
@ExceptionHandler(value = ApiException.class)
public CommonResult handle(ApiException e) {
if (e.getErrorCode() != null) {
return CommonResult.failed(e.getErrorCode());
}
return CommonResult.failed(e.getMessage());
}
}
配置相关
注解名称 | 注解位置 | 注解作用 |
---|---|---|
@Configuration | 类上 | 用于声明一个Java形式的配置类,在该类中声明的Bean等配置将被SpringBoot的组件扫描功能扫描到。 |
@EnableAutoConfiguration | 类上 | 启用SpringBoot的自动化配置,会根据你在pom.xml添加的依赖和application.yml中的配置自动创建你需要的配置。 |
@ComponentScan | 类上 | 启用SpringBoot的组件扫描功能,将自动装配和注入指定包下的Bean实例。 |
@SpringBootApplication | 类上 | 用于表示SpringBoot应用中的启动类,相当于**@Configuration、@EnableAutoConfiguration和@ComponentScan**三个注解的结合体。 |
@ConfigurationProperties (prefix = “”) | 类上 | 用于批量注入外部配置,以对象的形式来导入指定前缀的配置 |
测试相关
注解名称 | 注解位置 | 注解作用 |
---|---|---|
@Test | 方法上 | 表示该方法是一个测试方法,JUnit 会执行该方法并验证其行为是否符合预期 |
@BeforeAll | 方法上 | 该方法会在所有测试方法执行之前被执行一次,通常用于初始化一些昂贵的资源或者执行一些全局的设置 |
@AfterAll | 方法上 | 该方法会在所有测试方法执行完毕之后被执行一次,通常用于清理那些在 @BeforeAll 中初始化的资源 |
@BeforeEach | 方法上 | 该方法会在每个测试方法执行之前被执行,用于为每个测试用例进行初始化操作 |
@AfterEach | 方法上 | 该方法会在每个测试方法执行之后被执行,用于清理每个测试用例产生的资源或者状态 |
@ParameterizedTest | 方法上 | 用于定义参数化测试方法,即可以使用不同的参数多次运行同一个测试方法 |
@ValueSource | 方法上 | 与 @ParameterizedTest 配合使用,标注在方法上 提供一组单一类型的参数值(如整数、字符串等)给 @ParameterizedTest 标注的方法 |
@CsvSource | 方法上 | 与 @ParameterizedTest 配合使用,标注在方法上 从 CSV(逗号分隔值)格式的字符串中读取多组参数值,每组参数值可以包含多个值,提供给 @ParameterizedTest 标注的方法 |
//@ParameterizedTest 及 @ValueSource举例说明
public class MyTest {
@ParameterizedTest
@ValueSource(strings = {"value1", "value2", "value3"})
public void testWithValueSource(String input) {
// 在这里编写使用input进行测试的逻辑
}
}
//@ParameterizedTest 及 @ValueSource举例说明
public class MyTest {
@ParameterizedTest
@CsvSource({"input1, expectedOutput1", "input2, expectedOutput2"})
public void testWithCsvSource(String input, String expectedOutput) {
// 在这里编写使用input和expectedOutput进行测试的逻辑
}
AOP相关
注释名称 | 注释位置 | 注释作用 |
---|---|---|
@Aspect | 类上 | 用于定义切面,切面是通知和切点的结合,定义了何时、何地应用通知功能。 |
@Before | 方法上 | 表示前置通知(Before),通知方法会在目标方法调用之前执行,通知描述了切面要完成的工作以及何时执行。 |
@After | 方法上 | 表示后置通知(After),通知方法会在目标方法返回或抛出异常后执行。 |
@AfterReturning | 方法上 | 表示返回通知(AfterReturning),通知方法会在目标方法返回后执行。 |
@AfterThrowing | 方法上 | 表示异常通知(AfterThrowing),通知方法会在目标方法返回后执行。 |
@Around | 方法上 | 表示环绕通知(Around),通知方法会将目标方法封装起来,在目标方法调用之前和之后执行自定义的行为。 |
@Pointcut | 方法上 | 定义切点表达式,定义了通知功能被应用的范围。 |
@Order | 类上 | 用于定义组件的执行顺序,在AOP中指的是切面的执行顺序,value属性越低优先级越高。 |
//AOP相关注解案列 实现: 统一日志处理切面
@Aspect
@Component
@Order(1)
public class WebLogAspect {
private static final Logger LOGGER = LoggerFactory.getLogger(WebLogAspect.class);
@Pointcut("execution(public * com.macro.mall.tiny.controller.*.*(..))")
public void webLog() {
}
@Before("webLog()")
public void doBefore(JoinPoint joinPoint) throws Throwable {
}
@AfterReturning(value = "webLog()", returning = "ret")
public void doAfterReturning(Object ret) throws Throwable {
}
@Around("webLog()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
WebLog webLog = new WebLog();
//省略日志处理操作...
Object result = joinPoint.proceed();
LOGGER.info("{}", JSONUtil.parse(webLog));
return result;
}
}
Springboot常见方法
封装返回结果类
在一个项目中, 前后端经常会需要统一的数据格式实现交互, 封装一个实体类会使代码更易维护, 通常会包含 状态码(表示请求是否成功) , 消息(对请求结果的描述)和数据(实际的业务数据)。 举例如下:
@Data
public class Result {
private Integer code;
private String msg;
private Object data;
//返回成功 无数据传送
public static Result success(){
Result result = new Result();
result.code = 1;
result.msg = "success";
return result;
}
//返回成功信息 有数据传送
public static Result success(Object data){
Result result = new Result();
result.code = 1;
result.msg = "success";
result.data = data;
return result;
}
//返回失败信息 传输错误信息
public static Result error(String msg){
Result result = new Result();
result.code = 0;
result.msg = msg;
return result;
}
}
分页查询
分页查询是查询的常用操作, 这里推荐使用PageHelper插件实现, 这样就无需在Mapper中进行手动分页了,
如果前端传入参数过多时, 可以封装实体类来接收数据, 但要包装实体类变量名和前端对应
1.在pom.xml引入依赖
<!--分页插件PageHelper-->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.4.7</version>
</dependency>
2.封装接收参数实体类 , 变量名和请求参数一致
@Data
public class EmpQueryParam {
private Integer page = 1; //页码 默认为1
private Integer pageSize = 10; //每页展示记录数 默认为10
private String name; //姓名
private Integer gender; //性别
private LocalDate begin; //入职开始时间
private LocalDate end; //入职结束时间
}
3.封装返回值实体类 , 根据接口需要返回
@Data
@NoArgsConstructor
@AllArgsConstructor
public class PageBean {
private Long total; // 总记录数
private List rows; // 当前页数据列表
}
4.Controller中使用实体类接收多个参数
@GetMapping
public Result page(EmpQueryParam param) {
log.info("请求参数: {}", param);
PageBean pageBean = empService.page(param);
return Result.success(pageBean);
}
5.Service层
@Service
public PageBean getEmpsList(EmpQueryParam empQueryParam) {
PageHelper.startPage(empQueryParam.getPage(), empQueryParam.getPageSize()); //获取page和pageSize
List<Emp> empList = empMapper.getEmpsList(empQueryParam); //通过mapper获取查询列表
Page<Emp> p = (Page<Emp>) empList; //列表类型需要强转为 Page 类型
return new PageBean(p.getTotal(), p.getResult()); //返回总查询数 及 当前页数据列表
}
6.Mapper层 PageHelper会自动分页,不需要写limit , 这里使用.xml实现
List<Emp> getEmpsList(EmpQueryParam empQueryParam);
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<select id="getEmpsList" resultType="com..pojo.Emp"> <!--注意返回值类型-->
select *,dept.name deptName from emp left join dept on emp.dept_id = dept.id
<where>
<if test="name != null and name != ''">emp.name like concat('%',#{name},'%')</if>
<if test="gender != null">and gender = #{gender}</if>
<if test="begin != null and end != null">and entry_date between #{begin} and #{end}</if>
</where>
order by emp.update_time desc
</select>
主键 ID 获取
在进行员工工作经历插入操作时,我们遇到了一个关键问题。由于员工是新增记录,其在数据库中的 ID 是自动增长的主键。而工作经历的插入操作需要关联到员工的 ID,这就导致在执行工作经历添加操作时,无法直接获取到员工的 ID。
如果按照常规思路,先执行插入员工记录的操作,然后再通过查询操作获取刚刚插入员工的 ID,这种方式虽然能够解决获取 ID 的问题,但会带来效率方面的严重问题。
Spring 框架为我们提供了一个非常实用的注解 @Options , 能够有效解决上述问题
方法一: Mapper层直接在方法上使用注解
//useGeneratedKeys = true 表示开启获取自动生成键值的功能,keyProperty 属性则指定了要将获取到的键值设置到对象的哪个属性上
@Options(useGeneratedKeys = true, keyProperty = "自增主键")
方法二: .xml映射文件使用属性名
<insert id="Mapper的方法名" useGeneratedKeys="true" keyProperty="自增主键">
<!--SQL语句-->
</insert>
示例 :
@Service
public class Service {
@Autowired
private EmpMapper empMapper;
//员工表添加数据
public void add(Emp emp){
empMapper.add(emp)
//添加完可以直接获取ID
Integer id = emp.getId();
}
}
@Mapper //与.xml二选一
public interface EmpMapper {
@Option(useGeneratedKeys=true,keyProperty="id")
@Insert("insert into emp(username, name, gender, phone) values (#{username},#{name},#{gender},#{phone}}")
void add(Emp emp);
}
<!--与mapper二选一-->
<mapper namespace="com..mapper.EmpMapper" >
<insert id="add" useGeneratedKeys="true" keyProperty="id">
insert into emp(username, name, gender, phone)
values (#{username},#{name},#{gender},#{phone});
</insert>
事务
事务概念
事务是一组操作的集合,它是一个不可分割的工作单位。事务会把所有的操作作为一个整体一起向系统提交或撤销操作请求,即这些操作 要么同时成功,要么同时失败。
事务控制主要三步操作:开启事务、提交事务/回滚事务。
- 需要在这组操作执行之前,先开启事务 (
start transaction; / begin;
)。 - 所有操作如果全部都执行成功,则提交事务 (
commit;
)。 - 如果这组操作中,有任何一个操作执行失败,都应该回滚事务 (
rollback
)。
-- 开启事务
start transaction; / begin;
-- 1. 保存员工基本信息
insert into emp values (39, 'Tom', '123456');
-- 2. 保存员工的工作经历信息
insert into emp_expr(emp_id, begin, end) values (39,'2019-01-01', '2020-01-01');
-- 提交事务(全部成功)
commit;
-- 回滚事务(有一个失败)
rollback;
事务的特性 (ACID)
-
原子性(Atomicity) :原子性是指事务包装的一组sql是一个不可分割的工作单元,事务中的操作要么全部成功,要么全部失败。
-
一致性(Consistency):一个事务完成之后数据都必须处于一致性状态。
- 如果事务成功的完成,那么数据库的所有变化将生效。
- 如果事务执行出现错误,那么数据库的所有变化将会被回滚(撤销),返回到原始状态。
-
隔离性(Isolation):多个用户并发的访问数据库时,一个用户的事务不能被其他用户的事务干扰,多个并发的事务之间要相互隔离。
- 一个事务的成功或者失败对于其他的事务是没有影响。
-
持久性(Durability):一个事务一旦被提交或回滚,它对数据库的改变将是永久性的,哪怕数据库发生异常,重启之后数据亦然存在。
Spring 框架中的事务管理
@Transactional注解
使用位置
使用位置 | 注解作用 |
---|---|
类上 | 当前类中所有的方法都交由spring进行事务管理 (推荐) |
方法上 | 当前方法交给spring进行事务管理 |
接口上 | 接口下所有的实现类当中所有的方法都交给spring 进行事务管理 |
属性
属性 | 含义 | 作用 |
---|---|---|
rollbackFor | 指定事务在遇到哪些异常时应该回滚的属性 | 默认情况下,事务只会在遇到运行时异常(RuntimeException )及其子类时才会自动回滚,而对于受检异常(如IOException 、SQLException 等需要在方法签名中声明的异常),事务不会自动回滚。 |
propagation | ( 事务传播行为 ) 定义了在一个已经存在事务的情况下,新的事务应该如何与已有的事务进行交互 | 不同的传播行为决定了事务的边界和嵌套事务的处理方式 (具体如下表) |
@Transactional(rollbackFor = {SQLException.class, IOException.class})
public void someBusinessMethod() throws SQLException, IOException {
// 业务逻辑代码,可能会抛出SQLException或IOException
}
propagation
属性值 | 作用 | 举例 |
---|---|---|
REQUIRED (默认) | 如果当前没有事务,则创建一个新事务;如果当前已经存在事务,则加入到该事务中 | 在一个分层架构的应用中,一个业务方法调用另一个业务方法,并且都被标记为@Transactional (默认传播行为),那么内部方法将加入到外部方法已经开启的事务中,它们共享同一个事务。 |
REQUIRES_NEW | 总是创建一个新的事务,如果当前已经存在事务,则将当前事务挂起 | 在一个订单处理系统中,保存订单明细和更新订单状态可能需要在不同的事务中进行。保存订单明细的操作可能使用REQUIRES_NEW 传播行为,这样即使更新订单状态的事务失败,订单明细的保存事务也可以独立提交。 |
SUPPORTS | 如果当前存在事务,则加入到该事务中;如果当前没有事务,则以非事务方式执行 | 在一些辅助性的查询方法中,如果调用它的方法有事务,则该查询方法可以在事务环境下执行(可能受益于事务的隔离性等特性);如果调用它的方法没有事务,它也可以正常执行而不需要事务的支持。 |
NOT_SUPPORTED | 以非事务方式执行,如果当前存在事务,则将当前事务挂起 | 在进行一些日志记录或者缓存更新操作时,可能不需要事务的支持,使用NOT_SUPPORTED 传播行为可以避免事务管理的开销。 |
MANDATORY | 如果当前存在事务,则加入到该事务中;如果当前没有事务,则抛出异常 | 这种传播行为确保某个方法必须在事务环境下执行,常用于一些必须依赖事务的关键业务逻辑部分 |
NEVER | 以非事务方式执行,如果当前存在事务,则抛出异常 | 在某些特定的外部接口调用场景中,不允许在事务环境下执行,使用NEVER 传播行为可以防止意外的事务关联。 |
文件上传 (阿里云OSS)
文章默认在获取到 AccessKey 后开始下面步骤 , 实际开发当中,是需要从前往后仔细的去阅读官方文档的 , 本文篇幅原因只关注重点内容
官方文档 : 阿里云OSS帮助文档SDK参考JAVA:Java_对象存储(OSS)-阿里云帮助中心 (aliyun.com)
基础配置
配置AK & SK
以管理员身份打开CMD命令行,执行如下命令,配置系统的环境变量。
set OSS_ACCESS_KEY_ID=阿里云拿到的AccessKeyId
set OSS_ACCESS_KEY_SECRET=阿里云拿到的AccessKeySecret
执行如下命令,让更改生效。
setx OSS_ACCESS_KEY_ID "%OSS_ACCESS_KEY_ID%"
setx OSS_ACCESS_KEY_SECRET "%OSS_ACCESS_KEY_SECRET%"
验证是否生效,这个的好处是都存在了计算机上,不怕服务器被攻击看到源码
echo %OSS_ACCESS_KEY_ID%
echo %OSS_ACCESS_KEY_SECRET%
依赖配置
<!--阿里云OSS-->
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.17.4</version>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.1</version>
</dependency>
<dependency>
<groupId>javax.activation</groupId>
<artifactId>activation</artifactId>
<version>1.1.1</version>
</dependency>
<!-- no more than 2.3.3-->
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
<version>2.3.3</version>
</dependency>
application.yml配置
#阿里云OSS地址
aliyun:
oss:
endpoint: 阿里云端点
bucketName: 存储空间名称
#这两个一般不用写,一般配置在环境变量里面了。这两个是配置Access Key ID和Access Key Secret
accessKeyId: 从阿里云获取Access Key Id
accessKeySecret: 从阿里云获取Access Key secret
引入工具类和封装类
工具类AliyunOssUtils
@Slf4j
public class AliyunOSSUtils {
/**
* 上传文件
* @param endpoint endpoint域名
* @param bucketName 存储空间的名字
* @param content 内容字节数组
*/
public static String upload(String endpoint, String bucketName, byte[] content, String extName) throws Exception {
// 从环境变量中获取访问凭证。运行本代码示例之前,请确保已设置环境变量OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET。
EnvironmentVariableCredentialsProvider credentialsProvider = CredentialsProviderFactory.newEnvironmentVariableCredentialsProvider();
// 填写Object完整路径,完整路径中不能包含Bucket名称,例如exampledir/exampleobject.txt。
String objectName = UUID.randomUUID() + extName;
// 创建OSSClient实例。
OSS ossClient = new OSSClientBuilder().build(endpoint, credentialsProvider);
try {
// 创建PutObjectRequest对象。
PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, objectName, new ByteArrayInputStream(content));
// 创建PutObject请求。
PutObjectResult result = ossClient.putObject(putObjectRequest);
} catch (OSSException oe) {
log.error("Caught an OSSException, which means your request made it to OSS, but was rejected with an error response for some reason.");
log.error("Error Message:" + oe.getErrorMessage());
log.error("Error Code:" + oe.getErrorCode());
log.error("Request ID:" + oe.getRequestId());
log.error("Host ID:" + oe.getHostId());
} catch (ClientException ce) {
log.error("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.");
log.error("Error Message:" + ce.getMessage());
} finally {
if (ossClient != null) {
ossClient.shutdown();
}
}
return endpoint.split("//")[0] + "//" + bucketName + "." + endpoint.split("//")[1] + "/" + objectName;
}
}
封装类AliyunOSSProperties
该类也可以不写 , 主要是为了简化过多的配置项
注意: 变量名称需要和配置项的名称相同
@Data
@Component
@ConfigurationProperties(prefix = "aliyun.oss")//该注解从配置文件application.properties或者yml中找到并赋值给对应的值
public class AliyunOSSProperties {
private String endpoint;
private String bucketName;
}
MultipartFile 实现功能
MultipartFile是Spring框架中的一个接口,用于处理上传的文件。
MultipartFile定义了一下方法,可以轻松读取,验证,和保存上传的文件。
方法名 | 作用 |
---|---|
getOriginalFilename() | 返回上传文件的原始文件名 |
getSize() | 返回上传文件的大小(一字节为单位) |
getContentType() | 返回上传文件的MIME类型(如image/jpg) |
getBytes() | 将上传文件的内容读取为字节数组 |
transferTo(File dest) | 将上传文件的内容保存到指定的目标文件中 |
@RestController
public class Controller {
@Autowried
private AliyunOSSProperties aliyunOSSProperties;
@PostMapping("/upload")
public Result upload(MultipartFile file) throws Exception {
String extName = file.getOriginalFilename().substring(file.getOriginalFilename().lastIndexOf("."));//获得文件的后缀,例如.jpg
String endpoint = aliyunOSSProperties.getEndpoint();//获得阿里云端点号
String bucketName = aliyunOSSProperties.getBucketName();//获得阿里云存储空间名称
String url = AliyunOSSUtils.upload(endpoint, bucketName, file.getBytes(), extName);//上传文件,这个方法是自动生成一个UUID名字,所以只需要后缀名就可以了
return Result.success(url);//返回存储的url地址
}
}
全局异常处理
一个项目中功能会有很多,不免就会有很多异常。有编译时异常,也有运行时异常,也有自定义异常。但是每一个异常如果都要try catch处理,难免会导致代码量过大,可读性变差;如果我们只是一味地往上抛异常,又会导致一件事,异常只是抛出,没有做任何处理。因此需要一个对异常做集中处理的方式。
全局异常处理是一种在应用程序中统一处理异常的机制。它的目的是捕获应用程序中任何地方可能出现的异常,然后以一种统一、优雅的方式进行处理,而不是让异常直接暴露给用户或导致系统崩溃。
Spring提供了全局异常处理器,只需要我们定义一个类,在这个类上增加一个注解**@RestControllerAdvice**就是定义好了。
最好是新建一个Exception包来存储处理器和自定义的异常。
在全局异常处理器当中,需要定义方法来捕获异常,在方法上需要加上注解**@ExceptionHandler**,可以通过控制该注解的value属性来制定我们要捕获的是哪一类异常,如果什么value都不加,它会默认处理我们所有没有处理的异常,就是没有被try/catch处理的或者没有被本注解处理的异常。
示例:一个自定义异常 CustomerException.java
和全局处理异常 GlobalExceptionHandler.java
public class CustomerException extends RuntimeException {
String msg;
public CustomerException(String msg) {
this.msg = msg;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
}
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler
public Result exception(Exception e) {//如果项目内没有任何try/catch那么这个方法将会处理所有非CustomerException的异常
e.printStackTrace();
return Result.error("对不起,操作失败,请联系管理员");
}
@ExceptionHandler(CustomerException.class)
public Result exception(CustomerException e) {//这个方法将会处理所有的CustomerException异常
log.info(e.getMessage());
return Result.error(e.getMsg());
}
}
会话技术
会话是指在一段时间内,一个用户与服务器之间的交互过程。会话技术就是用于在这个交互过程中管理和跟踪用户状态、存储用户相关信息的技术。
在 Web 应用中,用户可能会在多个页面之间跳转,执行不同的操作。例如,在购物网站上,用户先浏览商品,然后将商品加入购物车,最后进行结算。会话技术能够在整个过程中跟踪用户的登录状态、购物车内容等信息,确保用户体验的连贯性。
Cookie
Cookie是在客户端上存储少量信息(最大4KB)的一种方法。
当客户端首次访问服务器时,服务器可能会设置一个Cookie,其中可能包含一个唯一标识符或其他信息。每当客户端向服务器发送请求时,都会自动包含任何已设置的Cookies。服务器可以读取这些Cookies,并根据它们来识别客户端或提供特定于该客户端的服务(如免登录,访问权限等)。
@RestController
public class Controller {
//设置Cookie
@GetMapping("/set")
public Result setCookie(HttpServletResponse response){
response.addCookie(new Cookie("key1","value1"));//只能传两个String类型
response.addCookie(new Cookie("key2","value2"));//可以设置多个cookie
return Result.success();
}
//获得Cookie
@GetMapping("/get")
public Result getCookie(HttpServletRequest request){
Cookie[] cookies = request.getCookies();
for(Cookie cookie : cookies){
System.out.println(cookie.getName() + ":" + cookie.getValue());//打印出来的就是key1:value1 key2:value2
//可以进行别的操作,判断等
}
return Result.success();
}
}
优点:
- 方便:Cookies 允许服务器保存用户的状态信息,如登录状态、偏好设置等,使得网站能够为用户提供个性化的体验。
- 无状态协议的支持:HTTP 协议本身是无状态的,Cookies 提供了这一种机制。
- 轻量级:Cookies 存储的数据量较小(4KB)。
- 支持:几乎所有的现代浏览器都支持Cookies(包括手机)
缺点:
- 安全问题:Cookies存储的是明文,在使用非加密的传输时候,可能会被截获
- 存储容量有限
- 隐私问题:因为Cookies会追踪用户行为,会导致一些用户禁用Cookies或者使用隐私模式浏览。
- 跨域限制:默认只能由设置它的站点访问,这限制了跨域数据共享的能力。当协议不同(例如HTTP和HTTPS),IP不同,端口不同都算跨域
Session
Session是服务器端会话跟踪技术,它是存在服务端的。Session就是基于Cookie实现的。
@RestController
public class Controller {
//设置session
@GetMapping("/set")
public Result setSession(HttpSession session){
session.setAttribute("key1","value1");
session.setAttribute("key2","value2");
return Result.success();
}
//获取session
@GetMapping("/get")
public Result getSession(HttpServletRequest request){
HttpSession session = request.getSession();
Object value1 = session.getAttribute("key1");//通过key1拿到了value1
Object value2 = session.getAttribute("key2");//通过key2拿到了value2
//可以进行别的操作
return Result.success();
}
}
优点:
- 安全性较高:因为存在了服务器,相比客户端更安全
- 存储容量更大:虽然存储容量取决于服务器配置,但是比Cookies大的多
- 不受客户端限制:即使用户禁用了Cookies,只要服务器能通过其他方式识别用户,Session可以正常工作
缺点:
- 服务器负载增加
- 跨域问题
- 维护复杂:需要考虑分布式环境中共享Session数据
- 被攻击风险:如果Session被攻击者猜出或者通过其他方式获得,就会发生Session攻击
JWT令牌
JWT是一种用于在网络上安全传输信息的令牌,通过数字签名的方式,以JSON对象为载体,在不同的服务终端之间安全地传输信息。
优势:
- JWT数据量小,传输时还可以压缩
- 可以被缓存,从而减少服务器请求次数,提高性能
- 支持多种签名算法
- 可以跨域认证
- 包含所有必要的认证信息,并且被签名保护,这意味着服务器不需要查询数据库来验证用户身份,减轻服务器存储压力
缺点:
- JWT的安全性高度依赖于密钥的安全管理。需要保管好密钥,不能丢失也不能泄露。
JWT基本配置
JWT:(官网:https://jwt.io)
- 第一部分:Header(头),记录令牌类型、签名算法等。
- 第二部分:Payload(有效载荷),携带一些自定义信息、默认信息等。
- 第三部分:Signature(签名),防止Token被篡改、确保安全性。将header、payload加入指定密钥,通过指定签名算法计算而来。
引入依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
环境变量
前面两条是配置环境变量 最后一条是验证环境变量是否配置成功
set JWT_SECRET=你想要的密钥
setx JWT_SECRET %JWT_SECRET%
echo %JWT_SECRET%
配置JWTUtils工具类
public class JwtUtils {
private static Long expire = 43200000L;//这个是TOKEN的有效期毫秒数,这个是12小时
private static String JWT_KEY = "JWT_SECRET";//如果配置了环境变量就这么写
//如果没配置环境变量就这么写
//private static String JWT_SECRET = "密钥"
//生成JWT令牌TOKEN
public static String generateJwt(Map<String, Object> claims) {
String signKey = System.getenv(JWT_KEY);//如果没配置环境变量就不用写了
String jwt = Jwts.builder()
.addClaims(claims)
.signWith(SignatureAlgorithm.HS256, signKey)
.setExpiration(new Date(System.currentTimeMillis() + expire))
.compact();
return jwt;
}
//解析JWT令牌TOKEN
public static Claims parseJWT(String jwt) {
String signKey = System.getenv(JWT_KEY);//如果没配置环境变量就不用写了
Claims claims = Jwts.parser()
.setSigningKey(signKey)
.parseClaimsJws(jwt)
.getBody();
return claims;
}
}
使用示例
生成token
public String getToken(){
Map<String,Object> data = new HashMap<>();
data.put(key1,value1);
data.put(key2,value2);
String jwt = JwtUtils.generateJwt(data);
return jwt;
}
解析token
public Result parstJwt(String jwt){
try{
JwtUtils.parstJwt(jwt);
}catch(Exception e){
//如果走到这一步就说明解析出错了
return Result.orrer("解析失败");
}
return Result.success();
}
过滤器
最好是新建一个软件包,名为filter,专门用来存储过滤器。然后在里面定义类,实现Filter接口,并重写其中的方法。
需要在实现Filter接口的类上添加@WebFilter注解,声明这个类作为 Web 应用程序中的过滤器,这个注解后面可以添加拦截的路径,如果是"/*"则是全部拦截。然后在启动器类上添加注解@ServletComponentScan ,这个注解用于指示容器扫描并自动部署在类路径中的 Servlet、过滤器(Filter)、监听器(Listener)和其他 Servlet 组件。
示例:这个示例是拦截了所有的非/login路径请求,如果有合法TOKEN就通过
@WebFilter("/*")
public class TestFilter implements Filter {//应该导入jakarta.servlet里面的Filter
//初始化方法,随着web服务器启动执行一次,因为接口里面写成了默认方法,可以不重写,也可以重写
public void init(FilterConfig filterConfig) throws ServletException {
Filter.super.init(filterConfig);
}
//这个必须重写,每次有请求来的时候都要先执行这一步
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {
//转换成HttpServlet
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
//获取请求的路径
String url = request.getRequestURL().toString();
System.out.println("url:"+url);
//如果请求是login直接放行
if (url.contains("/login")){
filterChain.doFilter(request, response);
return;
}
//获取请求头里面的token信息
String token = request.getHeader("token");
if (!StringUtils.hasLength(token)){
//设置响应码
response.setStatus(HttpStatus.SC_UNAUTHORIZED);
return;
}
//解析token
try {
JwtUtils.parseJWT(token);
} catch (Exception e) {
//说明token错误,或者过期,解析失败
response.setStatus(HttpStatus.SC_UNAUTHORIZED);
return;
}
//放行
filterChain.doFilter(request, response);
}
//销毁方法, web服务器关闭时执行一次, 因为接口里面写成了默认方法,可以不重写,也可以重写
public void destroy() {
Filter.super.destroy();
}
}
拦截器
拦截器Interceptor用于在请求到达目标控制器之前或之后执行某些操作。虽然 JavaWeb 中通常使用的是过滤器,而非拦截器,但它们的作用有些相似。Filter 可以在请求到达 Servlet 之前或响应离开 Servlet 之后进行干预,常用于执行一些预处理或后处理任务。
优点:
- 拦截器可以注册在特定的控制器或全局范围,具有更精准的控制。
- 提供了更多的回调方法,分别在请求处理的不同阶段执行。
拦截器配置
示例步骤:
1.创建一个拦截器Interceptor的软件包,里面存放拦截器类,类实现HandlerInterceptor接口,注意,需要加入容器注解@Component,里面三个接口可以都不重写,需要使用时添加即可。三个方法的执行时机见下表。
@Component
public class TokenInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//获取url
String url = request.getRequestURL().toString();
//判断url中是否包含login,如果包含,就放行,但是下面在配置文件里面也写了,所以这里都注释掉了
//if(url.contains("login")){ //登录请求
// return true; //ture就是放行,false就是不放行
//}
//获得TOKEN
String jwt = request.getHeader("token");
//这个StringUtils.hasLength()是获得里面的String字符串是否非空并且长度大于0,如果没有,就说明token不存在,返回错误结果
if(!StringUtils.hasLength(jwt)){
response.setStatus(HttpStatus.SC_UNAUTHORIZED);//这个返回的是401
return false;
}
//解析token
try {
JwtUtils.parseJWT(jwt);
} catch (Exception e) {
e.printStackTrace();
response.setStatus(HttpStatus.SC_UNAUTHORIZED);
return false;
}
//走到这一步都没被return说明传过来的请求token也是正确的,放行
return ture;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
}
}
方法名 | 执行时机 |
---|---|
preHandle() | 方法在请求处理之前被调用,即在控制器方法执行之前。实现处理器的预处理(如登录检查) |
postHandle() | 方法在控制器方法执行之后,但在视图渲染之前调用。只要preHandle()返回true,这个一定执行。 |
afterCompletion() | 方法在视图渲染完成后调用,即整个请求处理流程的最后阶段。不管preHandle()返回true还是false,这个一定执行。 |
2.创建一个config的软件包,里面存放配置类,类实现WebMvcConfigurer接口,并重写addInterceptors方法,值得注意的是,这个需要加入@Configuration注解,声明一个类作为配置类,通过该注解,可以以编程的方式定义和配置 Spring 应用程序中的依赖关系,这种方式称为基于 Java 的配置。
@Configuration
public class WebConfig implements WebMvcConfigurer {
//需要填充容器
@Autowired
private TokenInterceptor tokenInterceptor;
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(tokenInterceptor)
.addPathPatterns("/**")//addPathPatterns是添加拦截路径,写什么具体参考下表
.excludePathPatterns("/login");//excludePathPatterns是排除路径,添加不需要拦截的路径
}
}
AOP
AOP,面向切面编程,旨在提高应用内关注点的模块化能力,如日志记录,事务管理,错误处理等。解决了传统面向对象编程中,这些关注点散布在整个项目中,导致代码高度重合和耦合度。
记得在Aspect类上也要加@Component注解。
优点:
- 减少代码重复
- 增强代码可读性和和维护性
引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
切入点
execution
切点位置写在切面类的注解的后面,语法格式:
@注解("execution(访问修饰符 返回值 包名.类或接口.方法名(参数列表) throws 异常")
例如
@Around("execution(public com.example.pojo.User com.example.controller.UserController.findByNameAndPassword(String,String)) throws Exception")
注意:
- 修饰符(如:public,protected),可以省略
- 异常一般不写,交给方法处理
通配符:
*
:可以表示任意返回值,包名,类名,方法名,任意类型的一个参数,也可以统配包名,类名,方法名的一部分..
:可以表示任意层级的包,或任意类型,任意个数的参数
省略方法的修饰符号,省略异常,用*代替返回值类型,用…省略参数,然后查询所有find开头的方法,示例如下:
execution(* com.example.example.controller.UserController.find*(..))
根据业务不同,也可以使用&&、||、!来组成复杂的切入点表达式
@annotation
基于注解的方式来匹配切入点方法,需要自定义一个注解,需要匹配哪个方法,就在对应的方法上加上对应的注解。
例如我们自定义一个注解,这个注解的名字就是@Record:
@Target(ElementType.METHOD)
@Documented
@Retention(RetentionPolicy.RUNTIME )
public @interface Record {
}
@Aspect
@Component
public class TestAspect {
@Around("@annotation(com.example.anno.Record)")
public Object recordLog(ProceedingJoinPoint joinPoint) throws Throwable {
//这里可以写@Before注解完成的方法
try {
//这里写@Around(环绕开始部分)
//执行目标方法
Object result = pjp.proceed();
//这里写@Around(环绕结束部分)注解完成的方法(没出异常的情况)
//这里写@AfterReturning注解完成的方法
return result;
} catch (Throwable e) {
//这里写@Around(环绕结束部分)注解完成的方法(出异常的情况)
//这里写@AfterThrowing注解完成的方法
}
//这里写@After注解完成的方法
return null;
}
}
连接点
连接点(Join Point)是指程序执行过程中的一个特定点,比如方法的调用或异常的抛出。连接点是AOP框架可以插入额外行为的地方。在切面中,可以通过JoinPoint对象来访问这些连接点的信息。
示例 :
@Aspect
@Component
public class LogAspect {
// 定义切入点
@Pointcut("execution(* com.example.service.*.*(..))")
public void test() {}
// 前置通知
@Before("test()")
public void logBefore(JoinPoint joinPoint) {
//目标方法被调用前执行的方法
}
// 后置通知
@After("test()")
public void logAfter(JoinPoint joinPoint) {
//目标方法执行完毕后执行
}
// 返回后通知
@AfterReturning("test()")
public void logAfterReturning(JoinPoint joinPoint, Object result) {
//目标方法成功执行后的方法
}
// 异常抛出后通知
@AfterThrowing("test()")
public void logAfterThrowing(JoinPoint joinPoint, Throwable ex) {
//目标方法出现异常的方法
}
// 环绕通知
@Around("test()")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
//写环绕前的方法
Object result = joinPoint.proceed(); // 继续执行目标方法
//写环绕后的方法
return result;
}
}
ProceedingJoinPoint的方法
方法名称 | 方法作用 |
---|---|
getSignature() | 获得当前连接点的方法签名信息 |
getArgs() | 获得当前连接点的方法参数 |
getTarget() | 获得当前连接点的目标对象 |
getThis() | 获得当前连接点的代理对象 |
getStaticPart() | 获得当前连接点的代理对象 |
示例 :
//获得类名
String className = joinPoint.getTarget().getClass().getName();
//获得操作方法名
String methodName = joinPoint.getSignature().getName();
//获得参数
Object[] args = joinPoint.getArgs();Object[] args = joinPoint.getArgs();