Spring-Boot学习笔记

这个笔记是在自己学习的过程中根据实际用到的和学到的整理出来的,可能会有缺失,错误等,主要是给激励自己学习,遇到写不下去的情况给自己一个参考,请各位大佬发现问题提出问题时能嘴下留情,也希望多提建议,谢谢。
本笔记长期更新(更新日期2024年9月17日)

目录

固定格式参考

application.yml

使用注意事项:

  • 需要删除application.properties,SpringBoot项目会优先按照那个执行,如果不想删除重命名也是可以的
  • 创建一个SpringBoot项目一般是不带的,需要手动去创建一个,在模块或者项目的src/resources目录下

书写规范:

  1. 严格的缩进关系,代表同级,下一级,上一级
  2. 冒号后面跟一个空格再跟值
  3. 如果不知道yml怎么写,知道properties怎么写,这个就是把properties的每个分隔点.换成冒号:加换行,然后合并
spring:
  application:
    name: 模块或者项目名称
#数据库配置
  datasource:
    url: jdbc:mysql://localhost:3306/数据库名称
    #这个是8.0的配置,如果是5.0需要改成com.mysql.jdbc.Driver
    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日志打印
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: 地址

mapper.xml(详细操作见另一个文件XML数据库操作笔记)

书写规范

  1. 开始不能有任何空格,换行,必须保证顶格
  2. namespace里面写映射到的对应Mapper接口,从包名开始写,例如com.example.pojo.UserMapper
<?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">
<mapper namespace="">

</mapper>

application.propertes(和yml二选一)

尽量使用yml文件,这个也可以用,并且SpringBoot会优先找这个

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打印日志
logging.level.org.springframework.jdbc.support.JdbcTransactionManager = debug

#阿里云OSS配置
aliyun.oss.endpoint=阿里云端点
aliyun.oss.bucketName=存储空间名称

#设置启动端口
server.port=端口号

功能性封装类和工具类参考

Result类

一般用于封装,然后将数据发送给前端

注意事项:

前端接收的code也不一定是code,也有可能是flag,接收的也有概率是布尔值等,一定要随机应变,不要粘过去就是用

前端接收的数据不一定是data,也有接收list的,此时就需要把里面的data数据名改成前端接收的

@Data
public class Result {

    private Integer code; //编码:1成功,0为失败
    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 object) {
        Result result = new Result();
        result.data = object;
        result.code = 1;
        result.msg = "success";
        return result;
    }

    public static Result error(String msg) {
        Result result = new Result();
        result.msg = msg;
        result.code = 0;
        return result;
    }

}

PageBean类

一般用于分页功能

@Data
@NoArgsConstructor
@AllArgsConstructor
public class PageBean {
	private Long total; //总记录数
	private List rows; //当前页数据列表
}

AliyunOssUtils类(详细的阿里云操作见第九章阿里云OSS的使用)

当要使用阿里云OSS存储的时候使用这个工具类来快速完成向云端存储

注意事项:

  1. 需要本机环境变量中已经配置好了OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET
@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类(详细的阿里云操作见第九章阿里云OSS的使用)

也是用来简化阿里云配置的,这个会通过注解@ConfigurationProperties(prefix = “aliyun.oss”)从配置文件application.properties或者yml中找到并赋值给对应的值

注意事项:

  1. 需要在配置文件中配置好名称完全一致的值
  2. 这个是简化配置的,如果说配置了二十多个属性,有好几个用到阿里云OSS操作的类,总不能在里面一个个写,所以也需要在用到该类的里面使用@Autowried填装
@Data
@Component
@ConfigurationProperties(prefix = "aliyun.oss")
public class AliyunOSSProperties {
    private String endpoint;
    private String bucketName;
}

JWTUtils类

用于生成和解析TOKEN的,但是必须要用JWT令牌并且引入了正确的依赖

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;
    }
}

SpringBoot工程

创建一个SpringBoot工程

创建时选择Spring Web依赖自动配置一些基本的WEB环境,比如Tomcat服务器的集成,Spring MVC核心组件等

创建时选择Lombok 可以对封装类进行简化注解

如果需要用到MySql数据库操作和MyBatis勾选MySql DriverMyBatis Framework

**注意事项:**创建好的SpringBoot工程会有一个对应的启动器,这个要和软件包同级,不然Spring会找不到

SpringBoot层级结构

控制层Controller

主要负责接收用户请求、处理请求参数、调用适当的服务层(Service)方法,并将结果返回给用户。是用户界面与后端服务器的桥梁。

服务层Service

负责业务逻辑的实现,封装了与业务相关的处理和操作,处理业务规则、数据验证、计算、转换等操作。也会去数据访问层拿到需要的数据。

数据访问层Repository

主要是隐藏数据访问的底层细节,提供给上层(服务层)的一个简洁的接口。

Mapper

通常与MyBatis这样的框架结合使用。通过定义SQL映射来处理数据访问。

Dao

通常与ORM框架结合使用。将数据访问操作封装到DAO对象中,这些对象通常对应于数据库中的表或者集合。

实体层pojo/entity

不依赖于任何框架或特定API,仅仅是实现了JAVA语言特定的对象。

工具类Utils

一般是一些静态方法,用于提供各种通用功能或者简化某些常见的开发任务。这些工具类在整个应用程序中被广泛使用,提高代码的复用性和简洁性。

其他可能存在的软件包

因为每个项目的不同,还有可能存在异常包exception等。

常用注解

注解名称注解位置注解作用
@Component类上Spring能识别这个类并将其作为一个bean对象管理
@Controller,@Service,@Repository都集成了这个注解,一般用这几个
@Autowried类、方法、字段、构造函数或局部变量上当一个类需要另一个类的实例作为依赖时,可以使用此注解来告诉 Spring 容器自动注入正确的实例
@Resource(name=“指定注入的bean名称”)类、方法、字段、构造函数或局部变量上可以通过名称查找 Bean,如果未指定名称,则默认按照字段名进行装配
不能和Autowried一起使用处理多实现可以用
@Qualifier(value=“制定注入的bean名称”)类、方法、字段、构造函数或局部变量上用于解决自动装配时的歧义问题,即当存在多个候选 Bean 时,可以指定具体的 Bean 名称来进行装配
需要和Autowried一起使用处理多实现可以用
@Primary类上用于解决在同一个类型下存在多个候选 Bean 时,应该优先使用哪一个 Bean 的问题。
如果一个 Bean 被标记为 @Primary,那么当存在多个候选者时,Spring 会优先选择带有 @Primary 标记的 Bean 进行注入
@Slf4j类上来自 Lombok 库,用于自动生成一个 Logger实例
@Log类上可以由不同的日志库提供,例如 Log4j2,用于自动生成一个 Logger实例
@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;
}

Controller内常用注解

注解名称注解位置注解作用
@RestController类上复合注解,集成了@Controller和@ResponseBody的功能
@Controller类上用来接收HTTP请求,可以根据地址来判断执行哪个功能
@ResponseBody类上或者方法上和@RequestBody相反,这个是在JAVA向前端发送时将数据转换为JSON数据
@RequestMapping(“/路径地址”,method=RequestMethod.请求方式)类或者方法上基础的注解,用来指定控制器方法处理哪些HTTP请求,定义一个或者多个URL映射
@GetMapping类或者方法上专门用于处理Get请求
@PostMapping类或者方法上专门用于处理POST请求
@PutMapping类或者方法上专门用于处理Put请求
@DeleteMapping类或者方法上专门用于处理Delete请求
@RequestParam(value=“获取的值的键名”,defaultValue=“如果没有参数的默认值”,required=true/false)参数上从HTTP请求的查询字符串中获取参数,可以指定一个请求参数的名字,并且可以指定默认值,是否必须存在等属性。获得的方式类似于map,根据键获得值
@RequestBody参数上当客户端发送JSON或者其他格式的数据作为请求体时,Spring会尝试将主体的内容转换为相应的JAVA对象
@PathVariable()参数上用于将URL路径的模板变量绑定到方法参数上。

Service内常用注解

注解名称注解位置注解作用
@Service类上表明类是一个服务提供者
@Transactional类上或者方法上表名这个方法是事务或者整个类下所有方法都是事务

Mapper内常用注解

注解名称注解位置注解作用
@Mapper类上表明类是一个映射接口方法
@MapKey(“键名”)方法上声明Map的键名,不写也行
@Param(“需要传递的参数名”)参数上SpringBoot一般不用。在MyBatis框架中,这个注解用于在SQL映射文件中的预编译语句中传递多个参数
@Options(useGeneratedKeys=true/false,keyProperty=“键名”)方法上在完成数据库操作后会获取数据库自动成成的键值并将这个值设置到对应的对象属性上
@Result方法上描述单个列如何映射到 Java 对象的属性上。
@Results方法上用于包装一个或多个 @Result 注解,可以用来描述一个完整的映射规则集合。

Result和Results的用法示例:

就是在JAVA中使用注解完成应该在XML的resultMap标签的功能

@Results({
    @Result(column="user_id",property="userId",id=true),
    @Result(column="user_name",property="userName"),
    @Result(column="phone",property="phone")
})
public List<User> select();

Dao内常用注解

注解名称注解位置注解租用
@Respository类上表名类是一个数据访问层或持久化层的组件

封装类中常用注解

注解名称注解位置注解作用
@Data类上生成get set hashcode equals toString方法
@AllArgsConstructor类上生成全参构造
@NoArgsConstructor类上生成无参构造
@Getter类上生成Getter方法
@Setter类上生成Setter方法
@DataTimeFormat(pattern = “”)成员变量上按照pattern写的规则解析String文件变成LocalDateTime(LocalDate/LocalTime)

测试和方法执行

注解名称注解位置注解作用
@Test方法上用于标记一个方法作为测试方法。
@BeforeAll方法上在所有测试方法之前只执行一次的方法。
@AfterAll方法上在所有测试方法之后只执行一次的方法。
@BeforeEach方法上在每个测试方法之前执行一次的方法。
@AfterEach方法上在每个测试方法之后执行一次的方法。
@ParameterizedTest方法上JUnit5的注解,测试方法可以使用多个参数集来执行多次,每组参数对应一次执行
@ValueSource方法上提供一个或多个固定值作为参数化测试的参数源。
@CsvSource方法上提供 CSV 格式的字符串作为参数化测试的参数源。

@ParameterizedTest、@ValueSource和@CsvSource示例:

@ParameterizedTest
@ValueSource(ints = {1, 2, 3})
void testNumbers(int number) {
    System.out.println(number);//会执行三次,分别打印1,2,3
}

@ParameterizedTest
@CsvSource({"1, one", "2, two", "3, three"})
void testNumbers(String number, String word) {
    System.out.println(number + ": " + word);//会执行三次,分别打印1:one,2:two,3:three
}

异常(详细用法见第八章异常)

注解名称注解位置注解作用
@RestControllerAdvice类上该类中的方法将用于处理特定的异常
@ExceptionHandler(value=“异常类名”)方法上用于标记处理特定异常的方法。当应用程序抛出某种异常时,被注解的方法会被调用,并处理该异常。

过滤器和拦截器

注解名称注解位置注解作用
@WebFilter(“写拦截路径,如果是/*则代表全部拦截”)类上声明一个类作为 Web 应用程序中的过滤器
@ServletComponentScan类上用于指示容器扫描并自动部署在类路径中的 Servlet、过滤器(Filter)、监听器(Listener)和其他 Servlet 组件。
@Configuration类上用于声明一个类作为配置类,可以以编程的方式定义和配置 Spring 应用程序中的依赖关系,这种方式称为基于 Java 的配置。

分页查询

分页查询一般是同时包含分页和查询了

有两种接收数据的方法,一种是通过封装类获得,一种是通过一个个接收

接受前端发送的数据

通过封装类获取

建议是通过封装类的方式,这样再传入数据多的时候,不用一个个写接收,让Spring自己对应封装好,但是这样的话需要注意,名字和前端返回的必须完全一致

样例参考:
假如我们可以通过姓名,手机号查询,并且有分页

已知前端发送回来的数据如下

{
    "name":"张",
    "phone":1234,
    "page":1,
    "pageSize":5
}

所以后端封装类(UserQueryParam)参考

@Data
public class UserQueryParam {
    private Integer page = 1;//前端未传回时默认为第一页
    private Integer pageSize = 10;//前端未传回时默认为查找十条数据
    private String name;
    private String phone;
}

定义返回对象PageBean类

这个类也是要取决于前端写的接收数据的名称来定义,这个total也有写在别的地方的,视情况而定。有的是取res.data.list,也有res.data.data,也有res.data.rows。

下面这个例子是前端接收res.data.total和res.data.rows的例子。

@Data
@NoArgsConstructor
@AllArgsConstructor
public class PageBean {
	private Long total; //总记录数
	private List rows; //当前页数据列表,如果是list或者data成员变量的名要跟着变
}

Controller类

@RestController
@RequestMapping("/users")
public class Controller {
    
    @Autowried
    private Service service;
    
    public Result select(UserQueryParam param){
        //调用Service层进行数据处理并返回
        PageBean pageBean = service.select(param);
        return Result.success(pageBean);
    }
}

直接获取

因为同时是做了分页和查询,所以接受的参数写起来也会很麻烦,不建议这么写

查询一般是GET请求,请求参数一般就是跟在路径后面的字符串

例如:/users?page=1&pageSize=10

@RestController
@RequestMapping("/users")
public class Controller{
    
    @Autowried
    private Service service;
    
    @GetMapping
    public Result select(@RequestParam(defaultValue="1") Integer page,
                        @RequsetParam(defaultValue="10") Integer Pagesize,//这两个是做分页功能用到的
                        @RequestParam(required = false) String name,
                        @RequestParam(required = false) String phone){//这两个是做查询功能用到的
        //调用Service层进行数据处理并返回
        PageBean pageBean = service.select(page,pageSize,name,phone);
        return Resule.success(pageBean);
    }
}

定义返回对象PageBean类

这个类也是要取决于前端写的接收数据的名称来定义,这个total也有写在别的地方的,视情况而定。有的是取res.data.list,也有res.data.data,也有res.data.rows。

下面这个例子是前端接收res.data.total和res.data.rows的例子。

@Data
@NoArgsConstructor
@AllArgsConstructor
public class PageBean {
	private Long total; //总记录数
	private List rows; //当前页数据列表,如果是list或者data成员变量的名要跟着变
}

不使用插件的分页查询

5.1已经完成了从前端获取数据并发送到Service层了,接下来就是根据条件分页和查询了。

不使用插件的分页查询就是使用SQL语句提供的limit功能,Service层的实现类里需要提前计算limit里面的两个值。

Service的实现

@Service
public class Service {
    
	@Autowried
	private Mapper mapper;
	
	public PageBean select(Integer page,Integer pageSize,String name,String phone){
        //获取总记录数
        Long total = mapper.count();
        
        //获取结果列表
        Integer start = (page - 1) * pageSize;//这个计算是计算起始值,加上MySql的limit索引是从0开始的
        List<User> list = mapper.select(start,pageSize,name,phone);
        
        //封装结果返回
        return new PageBean(total,list);
    }
}

Mapper的实现

@Mapper
public class Mapper {
    
    public Long count();
    
    public List<User> select(Integer start,Integer pageSize,String name,String phone)
    
}

Mapper.xml的实现

主要就是倒数第三行那里的limit

<?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">
<mapper namespace="com.example.mapper.Mapper">
	<select id="count">
    	select count(*) from user
    </select>
    
    <select id="select" resultType="com.example.pojo.User">
    	select * from user
        <where>
        	<if test="name != null and name != ''">and name like concat('%','#{name}','%')</if>
            <if test="phone != null and phone != ''">and phone like concat('%','#{phone}','%')</if>
        </where>
        limit #{start},#{pageSize}
    </select>
</mapper>

使用插件PageHelper的分页查询

5.1已经完成了从前端获取数据并发送到Service层了,接下来就是根据条件分页和查询了。

这里使用了PageHelper工具,需要在POM加入如下依赖

<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper-spring-boot-starter</artifactId>
    <version>2.1.0</version>
</dependency>

Service的实现

@Service
public class Service {
    PageHelper.startPage(param.getPage(),param.getPageSize());//这个就是PageHelper提供的分页方法,并且下面就必须写查询方法,仅针对下面生效,务必注意书写位置
    List<User> userList = mapper.select(param);
    Page<User> list = (Page<User>) userList;//将List类型强制转换为Page类型
    return new PageBean(list.getTotal(),list.geResult());//调用Page的获取总页数和数据的方法
}

Mapper的实现

@Mapper
public class Mapper {
    List<User> select(UserQueryParam param);
}

Mapper.xml的实现,好处就是PageHelper会自动分页,不需要我们写limit

<?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">
<mapper namespace="com.example.mapper.Mapper">  
    <select id="select" resultType="com.example.pojo.User">
    	select * from user
        <where>
        	<if test="name != null and name != ''">and name like concat('%','#{param.name}','%')</if>
            <if test="phone != null and phone != ''">and phone like concat('%','#{param.phone}','%')</if>
        </where>
    </select>
</mapper>

一对多的多表操作

一对多的操作非常常见,例如员工和工作经历,一个员工可能会有0个,也可能一个,也可能多个工作经历。

这种情况一般前端传来的JSON里面还会有一个数组,这个数组里面又是一个JSON

关键点就是我们要插入的工作经历需要员工的ID,但是这个员工是新增,ID是自动增长的主键,导致我们在做添加操作时没有ID,就需要我们添加到数据库以后再获得这个员工的ID。

如果插入后再查询效率难免太低,所以Spring提供了一个注解@Options,这个注解在完成数据库操作后会获取数据库自动成成的键值并将这个值设置到对应的对象属性上。

**使用前提:**这个必须是主键,自增主键

1.第一种方法:在Mapper接口上使用注解

@Options(useGeneratedKeys=true,keyProperty="自增主键")

2.第二种方法:在XML映射文件中使用属性useGeneratedKeys和keyProperty

<insert id="Mapper的方法名" useGeneratedKeys="true" keyProperty="自增主键">
	<!--SQL语句-->
</insert>

示例:在这个示例中,当前端发送请求后,传入一个没有id的param,但是在mapper咋洗完成add方法后,所有的param都会给ID返回数据库对应的值。

@RestController
@RequestMapper("/users")
public class Controller {
    
    @Autowried
    private Service service;
    
    @PostMapping
    public Result add(Param param){
        service.add(param);
        return Result.success();
    }
}


@Service
public class Service {
    
    @Autowried
    private Mapper mapper;
    
    public void add(Param param){
        mapper.add(prarm);
    }
}

@Mapper
public class Mapper {
    
    @Option(useGeneratedKeys=true,keyProperty="id")//这个和XML内写两种方法二选一
    @Insert("insert into user(name,phone) values(#{name},#{phone})")//这个和XML内写两种方法二选一
    void add(Param param);
    
}

事务(更多请见单独文档事务)

事务的特性

概念: 事务是一组操作的集合,它是一个不可分割的工作单位。事务会把所有的操作作为一个整体一起向系统提交或撤销操作请求,即这些操作 要么同时成功,要么同时失败。

事务的四大特性(ACID)

  • 原子性(Atomicity) :事务是不可分割的最小单元,要么全部成功,要么全部失败
  • 一致性(Consistency):事务完成时,必须所有的数据都保持一致状态
  • 隔离性(Isolation):数据库系统提供的隔离机制,保证事务在不受外部并发操作影响的独立环境下运行
  • 持久性(Durability):事务一单提交或回滚,它对数据库中的数据改变就是永久的

Spring事务管理

Transactional注解

使用位置

位置作用
方法上将当前方法交给Spring进行事务管理
类上当前类中所有方法都交给Spring进行事务管理
接口上接口上所有的实现类当中的所有方法都交给Spring进行事务管理

属性

属性作用
rollbackFor默认情况下,只有出现RuntimeException(运行时异常)才会回滚事务,这个属性可以定义异常的class,指定出现何种异常才会回滚事务
propagation用来指定传播行为,具体值见下表

propagation值

假设现在有两个事务方法,一个A方法,一个B方法,两个方法都被@Transactional注解了,A方法中又调用了B方法,此时就会出现一个问题,B方法运行时,是加入A方法的事务还是新建一个事务?

属性值含义
REQUIRED【默认值】需要事务,有则加入,无则创建新事务
REQUIRES_NEW需要新事务,无论有无,总是创建新事务
SUPPORTS支持事务,有则加入,无则在无事务状态中运行
NOT_SUPPORTED不支持事务,在无事务状态下运行,如果当前存在已有事务,则挂起当前事务
MANDATORY必须有事务,否则抛异常
NEVER必须没事务,否则抛异常

假如说B方法也是操作数据表,比如转账,A是扣钱,B是加钱,A扣完了调用B,B这时候就得加入A这个事务中,出问题全都回滚,此时用默认值就可以;如果说B是日志记录,不管成功与否,都要记录本次操作,那么B就需要单独开启一个事务,A后续出问题不会回滚B,此时B的值就是REQUIRES_NEW。

唯一一个要注意的点就是在不改变rollbackFor的值的情况下只有在出现RuntimeExcrption也就是运行时异常才会回滚,如果出现编译时异常并且异常被触发是不会回滚的

异常

一个项目中功能会有很多,不免就会有很多异常。有编译时异常,也有运行时异常,也有自定义异常。但是每一个异常如果都要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;
    }
}


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());
    }

}

例如我们遇到的那个问题,删除班级的时候如果班级里面有人抛出异常并且返回前端该班级有人,不能删除就可以这么写:

throw new CustomerException("对不起,该班级下有学生,不能直接删除");

阿里云OSS的使用

首先,注册,开通服务等在此都不介绍。

阿里云OSS帮助文档SDK参考JAVA:Java_对象存储(OSS)-阿里云帮助中心 (aliyun.com)截止于2024年9月15日,本网站可以正常访问,如果不能正常访问请尝试在阿里云官网自行查找

我们需要在拿到AccessKey后再进行下面的步骤

配置环境变量

管理员身份运行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%

引入依赖

<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

这个类想不写的话也可以不写,只要给每个都要用OSS服务的实现类都配置一下里面的属性就可以了。

参考:

public class Test{
    
    @Value("${aliyun.oss.endpoint}")
    private String endpoint;
    @Value("${aliyun.oss.bucketName}")
    private String bucketName;
    
    //各种实现方法
}

注意事项:

  1. 需要在配置文件中配置好名称完全一致的值
  2. 这个是简化配置的,如果说配置了二十多个属性,有好几个用到阿里云OSS操作的类,总不能在里面一个个写,所以也需要在用到该类的里面使用@Autowried填装
@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)将上传文件的内容保存到指定的目标文件中

实现参考

假如现在要在Controller中实现上传方法

@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地址
    }
}

会话技术

JavaWeb中会话技术是用来处理HTTP协议无状态特性的解决方案。HTTP协议是一种无状态协议,这意味着每次客户端向服务器发起请求时,服务器将此请求视为独立的事件,不会记住前一次请求的信息。然而,在构建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>

似乎新版的jwt更换了新的写法,下面都是按照这个版本写的

配置环境变量(可以不配置,直接写在服务器里)

分别在管理员身份运行下面三条,前两条是加入环境变量,第三条是验证这个数据是否加入了环境变量 ,这个的好处是都存在了计算机上,不怕服务器被攻击看到源码。

 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是JavaWeb提供的三大组件之一(Servlet程序、Listener监听器、Filter过滤器)

作用:拦截请求,过滤响应,一般用于登录校验,权限检查,日志操作,事务管理,敏感字符处理等。

过滤器和拦截器有一个不同的是过滤器在拦截器前面,相当于在TOMCAT里面,客户端发送的数据需要先经过过滤器,才能再经过拦截器(拦截器见下一章)

优点:

  • 可以在多个 Servlet 中使用相同的 Filter 实现相同的功能。
  • Filter 是独立的组件,可以方便地进行修改和替换。

定义过滤器

最好是新建一个软件包,名为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是排除路径,添加不需要拦截的路径
    }
}

拦截路径意义
/**所有子路径,包括二级,三级路径
/*只能拦截一集子路径,如果有二级路径则没法拦截
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值