Springboot学习笔记

基础配置

常用依赖

  • 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)及其子类时才会自动回滚,而对于受检异常(如IOExceptionSQLException等需要在方法签名中声明的异常),事务不会自动回滚。
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();
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值