首页
关于
Search
1
微服务
34 阅读
2
同步本地Markdown至Typecho站点
29 阅读
3
JavaWeb——后端
18 阅读
4
苍穹外卖
14 阅读
5
智能协同云图库
13 阅读
后端学习
项目
杂项
科研
论文
默认分类
登录
找到
17
篇与
后端学习
相关的结果
- 第 2 页
2025-05-21
Mybatis&-Plus
Mybatis 快速创建 创建springboot工程(Spring Initializr),并导入 mybatis的起步依赖、mysql的驱动包。创建用户表user,并创建对应的实体类User 在springboot项目中,可以编写main/resources/application.properties文件,配置数据库连接信息。 #驱动类名称 spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver #数据库连接的url spring.datasource.url=jdbc:mysql://localhost:3306/mybatis #连接数据库的用户名 spring.datasource.username=root #连接数据库的密码 spring.datasource.password=1234 在引导类所在包下,在创建一个包 mapper。在mapper包下创建一个接口 UserMapper @Mapper注解:表示是mybatis中的Mapper接口 -程序运行时:框架会自动生成接口的实现类对象(代理对象),并交给Spring的IOC容器管理 @Select注解:代表的就是select查询,用于书写select查询语句 @Mapper public interface UserMapper { //查询所有用户数据 @Select("select * from user") public List<User> list(); } 数据库连接池 数据库连接池是一个容器,负责管理和分配数据库连接(Connection)。 在程序启动时,连接池会创建一定数量的数据库连接。 客户端在执行 SQL 时,从连接池获取连接对象,执行完 SQL 后,将连接归还给连接池,以供其他客户端复用。 如果连接对象长时间空闲且超过预设的最大空闲时间,连接池会自动释放该连接。 优势:避免频繁创建和销毁连接,提高数据库访问效率。 Druid(德鲁伊) Druid连接池是阿里巴巴开源的数据库连接池项目 功能强大,性能优秀,是Java语言最好的数据库连接池之一 把默认的 Hikari 数据库连接池切换为 Druid 数据库连接池: 在pom.xml文件中引入依赖 <dependency> <!-- Druid连接池依赖 --> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.2.8</version> </dependency> 在application.properties中引入数据库连接配置 spring.datasource.druid.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.druid.url=jdbc:mysql://localhost:3306/mybatis spring.datasource.druid.username=root spring.datasource.druid.password=123456 SQL注入问题 SQL注入:由于没有对用户输入进行充分检查,而SQL又是拼接而成,在用户输入参数时,在参数中添加一些SQL关键字,达到改变SQL运行结果的目的,也可以完成恶意攻击。 在Mybatis中提供的参数占位符有两种:${...} 、#{...} #{...} 执行SQL时,会将#{…}替换为?,生成预编译SQL,会自动设置参数值 使用时机:参数传递,都使用#{…} ${...} 拼接SQL。直接将参数拼接在SQL语句中,存在SQL注入问题 使用时机:如果对表名、列表进行动态设置时使用 日志输出 只建议开发环境使用:在Mybatis当中我们可以借助日志,查看到sql语句的执行、执行传递的参数以及执行结果 打开application.properties文件 开启mybatis的日志,并指定输出到控制台 #指定mybatis输出日志的位置, 输出控制台 mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl 驼峰命名法 在 Java 项目中,数据库表字段名一般使用 下划线命名法(snake_case),而 Java 中的变量名使用 驼峰命名法(camelCase)。 小驼峰命名(lowerCamelCase): 第一个单词的首字母小写,后续单词的首字母大写。 例子:firstName, userName, myVariable 大驼峰命名(UpperCamelCase): 每个单词的首字母都大写,通常用于类名或类型名。 例子:MyClass, EmployeeData, OrderDetails 表中查询的数据封装到实体类中 实体类属性名和数据库表查询返回的字段名一致,mybatis会自动封装。 如果实体类属性名和数据库表查询返回的字段名不一致,不能自动封装。 解决方法: 起别名 结果映射 开启驼峰命名 属性名和表中字段名保持一致 开启驼峰命名(推荐):如果字段名与属性名符合驼峰命名规则,mybatis会自动通过驼峰命名规则映射 驼峰命名规则: abc_xyz => abcXyz 表中字段名:abc_xyz 类中属性名:abcXyz 推荐的完整配置: mybatis: #mapper配置文件 mapper-locations: classpath:mapper/*.xml type-aliases-package: com.sky.entity configuration: #开启驼峰命名 map-underscore-to-camel-case: true type-aliases-package: com.sky.entity把 com.sky.entity 包下的所有类都当作别名注册,XML 里就可以直接写 <resultType="Dish"> 而不用写全限定名。可以多添加几个包,用逗号隔开。 增删改 增删改通用!:返回值为int时,表示影响的记录数,一般不需要可以设置为void! 作用于单个字段 @Mapper public interface EmpMapper { //SQL语句中的id值不能写成固定数值,需要变为动态的数值 //解决方案:在delete方法中添加一个参数(用户id),将方法中的参数,传给SQL语句 /** * 根据id删除数据 * @param id 用户id */ @Delete("delete from emp where id = #{id}")//使用#{key}方式获取方法中的参数值 public void delete(Integer id); } 上图参数值分离,有效防止SQL注入 作用于多个字段 @Mapper public interface EmpMapper { //会自动将生成的主键值,赋值给emp对象的id属性 @Options(useGeneratedKeys = true,keyProperty = "id") @Insert("insert into emp(username, name, gender, image, job, entrydate, dept_id, create_time, update_time) values (#{username}, #{name}, #{gender}, #{image}, #{job}, #{entrydate}, #{deptId}, #{createTime}, #{updateTime})") public void insert(Emp emp); } 在 @Insert 注解中使用 #{} 来引用 Emp 对象的属性,MyBatis 会自动从 Emp 对象中提取相应的字段并绑定到 SQL 语句中的占位符。 @Options(useGeneratedKeys = true, keyProperty = "id") 这行配置表示,插入时自动生成的主键会赋值给 Emp 对象的 id 属性。 // 调用 mapper 执行插入操作 empMapper.insert(emp); // 现在 emp 对象的 id 属性会被自动设置为数据库生成的主键值 System.out.println("Generated ID: " + emp.getId()); 查 查询案例: 姓名:要求支持模糊匹配 性别:要求精确匹配 入职时间:要求进行范围查询 根据最后修改时间进行降序排序 重点在于模糊查询时where name like '%#{name}%' 会报错。 解决方案: 使用MySQL提供的字符串拼接函数:concat('%' , '关键字' , '%') CONCAT() 如果其中任何一个参数为 NULL,CONCAT() 返回 NULL,Like NULL会导致查询不到任何结果! NULL和''是完全不同的 @Mapper public interface EmpMapper { @Select("select * from emp " + "where name like concat('%',#{name},'%') " + "and gender = #{gender} " + "and entrydate between #{begin} and #{end} " + "order by update_time desc") public List<Emp> list(String name, Short gender, LocalDate begin, LocalDate end); } XML配置文件规范 使用Mybatis的注解方式,主要是来完成一些简单的增删改查功能。如果需要实现复杂的SQL功能,建议使用XML来配置映射语句,也就是将SQL语句写在XML配置文件中。 在Mybatis中使用XML映射文件方式开发,需要符合一定的规范: XML映射文件的名称与Mapper接口名称一致,并且将XML映射文件和Mapper接口放置在相同包下(同包同名) XML映射文件的namespace属性为Mapper接口全限定名一致 XML映射文件中sql语句的id与Mapper接口中的方法名一致,并保持返回类型一致。 <select>标签:就是用于编写select查询语句的。 resultType属性,指的是查询返回的单条记录所封装的类型(查询必须)。 parameterType属性(可选,MyBatis 会根据接口方法的入参类型(比如 Dish 或 DishPageQueryDTO)自动推断),POJO作为入参,需要使用全类名或是type‑aliases‑package: com.sky.entity 下注册的别名。 <insert id="insert" useGeneratedKeys="true" keyProperty="id"> <select id="pageQuery" resultType="com.sky.vo.DishVO"> <select id="list" resultType="com.sky.entity.Dish" parameterType="com.sky.entity.Dish"> 实现过程: resources下创与java下一样的包,即edu/whut/mapper,新建xx.xml文件 配置Mapper文件 <?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="edu.whut.mapper.EmpMapper"> <!-- SQL 查询语句写在这里 --> </mapper> namespace 属性指定了 Mapper 接口的全限定名(即包名 + 类名)。 编写查询语句 <select id="list" resultType="edu.whut.pojo.Emp"> select * from emp where name like concat('%',#{name},'%') and gender = #{gender} and entrydate between #{begin} and #{end} order by update_time desc </select> id="list":指定查询方法的名称,应该与 Mapper 接口中的方法名称一致。 resultType="edu.whut.pojo.Emp":resultType 只在 查询操作 中需要指定。指定查询结果映射的对象类型,这里是 Emp 类。 这里有bug!!! concat('%',#{name},'%')这里应该用<where> <if>标签对name是否为NULL或''进行判断 动态SQL SQL-if,where <if>:用于判断条件是否成立。使用test属性进行条件判断,如果条件为true,则拼接SQL。 <if test="条件表达式"> 要拼接的sql语句 </if> <where>只会在子元素有内容的情况下才插入where子句,而且会自动去除子句的开头的AND或OR,加了总比不加好 <select id="list" resultType="com.itheima.pojo.Emp"> select * from emp <where> <!-- if做为where标签的子元素 --> <if test="name != null"> and name like concat('%',#{name},'%') </if> <if test="gender != null"> and gender = #{gender} </if> <if test="begin != null and end != null"> and entrydate between #{begin} and #{end} </if> </where> order by update_time desc </select> SQL-foreach Mapper 接口 @Mapper public interface EmpMapper { //批量删除 public void deleteByIds(List<Integer> ids); } XML 映射文件 <foreach> 标签用于遍历集合,常用于动态生成 SQL 语句中的 IN 子句、批量插入、批量更新等操作。 <foreach collection="集合名称" item="集合遍历出来的元素/项" separator="每一次遍历使用的分隔符" open="遍历开始前拼接的片段" close="遍历结束后拼接的片段"> </foreach> open="(":这个属性表示,在生成的 SQL 语句开始时添加一个 左括号 (。 close=")":这个属性表示,在生成的 SQL 语句结束时添加一个 右括号 )。 例:批量删除实现 <delete id="deleteByIds"> DELETE FROM emp WHERE id IN <foreach collection="ids" item="id" separator="," open="(" close=")"> #{id} </foreach> </delete> 实现效果类似:DELETE FROM emp WHERE id IN (1, 2, 3); Mybatis-Plus MyBatis-Plus 的使命就是——在保留 MyBatis 灵活性的同时,大幅减少模板化、重复的代码编写,让增删改查、分页等常见场景“开箱即用”,以更少的配置、更少的样板文件、更高的开发效率,帮助团队快速交付高质量的数据库访问层。 快速开始 1.引入依赖 <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <!-- <dependency>--> <!-- <groupId>org.mybatis.spring.boot</groupId>--> <!-- <artifactId>mybatis-spring-boot-starter</artifactId>--> <!-- <version>2.3.1</version>--> <!-- </dependency>--> 由于这个starter包含对mybatis的自动装配,因此完全可以替换掉Mybatis的starter。 2.定义mapper 为了简化单表CRUD,MybatisPlus提供了一个基础的BaseMapper接口,其中已经实现了单表的CRUD(增删查改): 仅需让自定义的UserMapper接口,继承BaseMapper接口: public interface UserMapper extends BaseMapper<User> { } 测试: @SpringBootTest class UserMapperTest { @Autowired private UserMapper userMapper; @Test void testInsert() { User user = new User(); user.setId(5L); user.setUsername("Lucy"); user.setPassword("123"); user.setPhone("18688990011"); user.setBalance(200); user.setInfo("{\"age\": 24, \"intro\": \"英文老师\", \"gender\": \"female\"}"); user.setCreateTime(LocalDateTime.now()); user.setUpdateTime(LocalDateTime.now()); userMapper.insert(user); } @Test void testSelectById() { User user = userMapper.selectById(5L); System.out.println("user = " + user); } @Test void testSelectByIds() { List<User> users = userMapper.selectBatchIds(List.of(1L, 2L, 3L, 4L, 5L)); users.forEach(System.out::println); } @Test void testUpdateById() { User user = new User(); user.setId(5L); user.setBalance(20000); userMapper.updateById(user); } @Test void testDelete() { userMapper.deleteById(5L); } } 3.常见注解 MybatisPlus如何知道我们要查询的是哪张表?表中有哪些字段呢? 约定大于配置 泛型中的User就是与数据库对应的PO. MybatisPlus就是根据PO实体的信息来推断出表的信息,从而生成SQL的。默认情况下: MybatisPlus会把PO实体的类名驼峰转下划线作为表名 UserRecord->user_record MybatisPlus会把PO实体的所有变量名驼峰转下划线作为表的字段名,并根据变量类型推断字段类型 MybatisPlus会把名为id的字段作为主键 但很多情况下,默认的实现与实际场景不符,因此MybatisPlus提供了一些注解便于我们声明表信息。 @TableName 描述:表名注解,标识实体类对应的表 @TableId 描述:主键注解,标识实体类中的主键字段 TableId注解支持两个属性: 属性 类型 必须指定 默认值 描述 value String 否 "" 主键字段名 type Enum 否 IdType.NONE 指定主键类型 @TableName("user_detail") public class User { @TableId(value="id_dd",type=IdType.AUTO) private Long id; private String name; } 这个例子会,映射到数据库中的user_detail表,主键为id_dd,并且插入时采用数据库自增;能自动回写主键,相当于开启useGeneratedKeys=true,执行完 insert(user) 后,user.getId() 就会是数据库分配的主键值,否则默认获得null,但不影响数据表中的内容。 type=dType.ASSIGN_ID 表示用雪花算法生成密码,更加复杂,而不是简单的AUTO自增。它也能自动回写主键。 @TableField 普通字段注解 一般情况下我们并不需要给字段添加@TableField注解,一些特殊情况除外: 成员变量名与数据库字段名不一致 成员变量是以isXXX命名,按照JavaBean的规范,MybatisPlus识别字段时会把is去除,这就导致与数据库不符。 public class User { private Long id; private String name; private Boolean isActive; // 按 JavaBean 习惯,这里用 isActive,数据表是is_acitive,但MybatisPlus会识别为active } 成员变量名与数据库一致,但是与数据库的**关键字(如order)**冲突。 public class Order { private Long id; private Integer order; // 名字和 SQL 关键字冲突 } 默认MP会生成:SELECT id, order FROM order; 导致报错 一些字段不希望被映射到数据表中,不希望进行增删查改 解决办法: @TableField("is_active") private Boolean isActive; @TableField("`order`") //添加转义字符 private Integer order; @TableField(exist=false) //exist默认是true, private String address; 4.常用配置 大多数的配置都有默认值,因此我们都无需配置。但还有一些是没有默认值的,例如: 实体类的别名扫描包 全局id类型 要改也就改这两个即可 mybatis-plus: type-aliases-package: edu.whut.mp.domain.po global-config: db-config: id-type: auto # 全局id类型为自增长 作用:1.把edu.whut.mp.domain.po 包下的所有 PO 类注册为 MyBatis 的 Type Alias。这样在你的 Mapper XML 里就可以直接写 <resultType="User">(或 <parameterType="User">)而不用写全限定类名 edu.whut.mp.domain.po.User 2.无需在每个 @TableId 上都写 type = IdType.AUTO,统一由全局配置管。 核心功能 前面的例子都是根据主键id更新、修改、查询,无法支持复杂条件where。 条件构造器Wrapper 除了新增以外,修改、删除、查询的SQL语句都需要指定where条件。因此BaseMapper中提供的相关方法除了以id作为where条件以外,还支持更加复杂的where条件。 Wrapper就是条件构造的抽象类,其下有很多默认实现,继承关系如图: QueryWrapper 在AbstractWrapper的基础上拓展了一个select方法,允许指定查询字段,无论是修改、删除、查询,都可以使用QueryWrapper来构建查询条件。 select方法只需用于 查询 时指定所需的列,完整查询不需要,用于update和delete不需要。 QueryWrapper 里对 like、eq、ge 等方法都做了重载 QueryWrapper<User> qw = new QueryWrapper<>(); qw.like("name", name); //两参版本,第一个参数对应数据库中的列名,如果对应不上,就会报错!!! qw.like(StrUtil.isNotBlank(name), "name", name); //三参,多一个boolean condition 参数 **例1:**查询出名字中带o的,存款大于等于1000元的人的id,username,info,balance: /** * SELECT id,username,info,balance * FROM user * WHERE username LIKE ? AND balance >=? */ @Test void testQueryWrapper(){ QueryWrapper<User> wrapper =new QueryWrapper<User>() .select("id","username","info","balance") .like("username","o") .ge("balance",1000); //查询 List<User> users=userMapper.selectList(wrapper); users.forEach(System.out::println); } UpdateWrapper 基于BaseMapper中的update方法更新时只能直接赋值,对于一些复杂的需求就难以实现。 例1: 例如:更新id为1,2,4的用户的余额,扣200,对应的SQL应该是: UPDATE user SET balance = balance - 200 WHERE id in (1, 2, 4) @Test void testUpdateWrapper() { List<Long> ids = List.of(1L, 2L, 4L); // 1.生成SQL UpdateWrapper<User> wrapper = new UpdateWrapper<User>() .setSql("balance = balance - 200") // SET balance = balance - 200 .in("id", ids); // WHERE id in (1, 2, 4) // 2.更新,注意第一个参数可以给null,告诉 MP:不要从实体里取任何字段值 // 而是基于UpdateWrapper中的setSQL来更新 userMapper.update(null, wrapper); } 例2: // 用 UpdateWrapper 拼 WHERE + SET UpdateWrapper<User> wrapper = new UpdateWrapper<User>() // WHERE status = 'ACTIVE' .eq("status", "ACTIVE") // SET balance = 2000, name = 'Alice' .set("balance", 2000) .set("name", "Alice"); // 把 entity 参数传 null,MyBatis-Plus 会只用 wrapper 里的 set/where userMapper.update(null, wrapper); LambdaQueryWrapper(推荐) 是QueryWrapper和UpdateWrapper的上位选择!!! 传统的 QueryWrapper/UpdateWrapper 需要把数据库字段名写成字符串常量,既容易拼写出错,也无法在编译期校验。MyBatis-Plus 引入了两种基于 Lambda 的 Wrapper —— LambdaQueryWrapper 和 LambdaUpdateWrapper —— 通过传入实体类的 getter 方法引用,框架会自动解析并映射到对应的列,实现了类型安全和更高的可维护性。 // ——— 传统 QueryWrapper ——— public User findByUsername(String username) { QueryWrapper<User> qw = new QueryWrapper<>(); // 硬编码列名,拼写错了编译不过不了,会在运行时抛数据库异常 qw.eq("user_name", username); return userMapper.selectOne(qw); } // ——— LambdaQueryWrapper ——— public User findByUsername(String username) { // 内部已注入实体 Class 和元数据,方法引用自动解析列名 LambdaQueryWrapper<User> qw = Wrappers.lambdaQuery(User.class) .eq(User::getUserName, username); return userMapper.selectOne(qw); } 自定义sql 即自己编写Wrapper查询条件,再结合Mapper.xml编写SQL **例1:**以 UPDATE user SET balance = balance - 200 WHERE id in (1, 2, 4) 为例: 1)先在业务层利用wrapper创建条件,传递参数 @Test void testCustomWrapper() { // 1.准备自定义查询条件 List<Long> ids = List.of(1L, 2L, 4L); QueryWrapper<User> wrapper = new QueryWrapper<User>().in("id", ids); // 2.调用mapper的自定义方法,直接传递Wrapper userMapper.deductBalanceByIds(200, wrapper); } 2)自定义mapper层把wrapper和其他业务参数传进去,自定义sql语句书写sql的前半部分,后面拼接。 public interface UserMapper extends BaseMapper<User> { /** * 注意:更新要用 @Update * - #{money} 会被替换为方法第一个参数 200 * - ${ew.customSqlSegment} 会展开 wrapper 里的 WHERE 子句 */ @Update("UPDATE user " + "SET balance = balance - #{money} " + "${ew.customSqlSegment}") void deductBalanceByIds(@Param("money") int money, @Param("ew") QueryWrapper<User> wrapper); } @Param("ew")就是给这个方法参数在 MyBatis 的 SQL 映射里起一个别名—— ew , Mapper 的注解或 XML 里,MyBatis 想要拿到这个参数,就用它的 @Param 名称——也就是 ew: @Param("ew")中ew是 MP 约定的别名! ${ew.customSqlSegment} 可以自动拼接传入的条件语句 **例2:**查询出所有收货地址在北京的并且用户id在1、2、4之中的用户 普通mybatis: <select id="queryUserByIdAndAddr" resultType="com.itheima.mp.domain.po.User"> SELECT * FROM user u INNER JOIN address a ON u.id = a.user_id WHERE u.id <foreach collection="ids" separator="," item="id" open="IN (" close=")"> #{id} </foreach> AND a.city = #{city} </select> mp方法: @Test void testCustomJoinWrapper() { // 1.准备自定义查询条件 QueryWrapper<User> wrapper = new QueryWrapper<User>() .in("u.id", List.of(1L, 2L, 4L)) .eq("a.city", "北京"); // 2.调用mapper的自定义方法 List<User> users = userMapper.queryUserByWrapper(wrapper); } @Select("SELECT u.* FROM user u INNER JOIN address a ON u.id = a.user_id ${ew.customSqlSegment}") List<User> queryUserByWrapper(@Param("ew")QueryWrapper<User> wrapper); Service层的常用方法 查询: selectById:根据主键 ID 查询单条记录。 selectBatchIds:根据主键 ID 批量查询记录。 selectOne:根据指定条件查询单条记录。 @Service public class UserService { @Autowired private UserMapper userMapper; public User findByUsername(String username) { QueryWrapper<User> queryWrapper = new QueryWrapper<>(); queryWrapper.eq("username", username); return userMapper.selectOne(queryWrapper); } } selectList:根据指定条件查询多条记录。 QueryWrapper<User> queryWrapper = new QueryWrapper<>(); queryWrapper.ge("age", 18); List<User> users = userMapper.selectList(queryWrapper); 插入: insert:插入一条记录。 User user = new User(); user.setUsername("alice"); user.setAge(20); int rows = userMapper.insert(user); 更新 updateById:根据主键 ID 更新记录。 User user = new User(); user.setId(1L); user.setAge(25); int rows = userMapper.updateById(user); update:根据指定条件更新记录。 UpdateWrapper<User> updateWrapper = new UpdateWrapper<>(); updateWrapper.eq("username", "alice"); User user = new User(); user.setAge(30); int rows = userMapper.update(user, updateWrapper); 删除操作 deleteById:根据主键 ID 删除记录。 deleteBatchIds:根据主键 ID 批量删除记录。 delete:根据指定条件删除记录。 QueryWrapper<User> queryWrapper = new QueryWrapper<>(); queryWrapper.eq("username", "alice"); int rows = userMapper.delete(queryWrapper); IService 基本使用 由于Service中经常需要定义与业务有关的自定义方法,因此我们不能直接使用IService,而是自定义Service接口,然后继承IService以拓展方法。同时,让自定义的Service实现类继承ServiceImpl,这样就不用自己实现IService中的接口了。 首先,定义IUserService,继承IService: public interface IUserService extends IService<User> { // 拓展自定义方法 } 然后,编写UserServiceImpl类,继承ServiceImpl,实现UserService: @Service public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService { } Controller层中写: @RestController @RequestMapping("/users") @Slf4j @Api(tags = "用户管理接口") public class UserController { @Autowired private IUserService userService; @PostMapping @ApiOperation("新增用户接口") public void saveUser(@RequestBody UserFormDTO userFormDTO){ User user=new User(); BeanUtils.copyProperties(userFormDTO, user); userService.save(user); } @DeleteMapping("{id}") @ApiOperation("删除用户接口") public void deleteUserById(@PathVariable Long id){ userService.removeById(id); } @GetMapping("{id}") @ApiOperation("根据id查询接口") public UserVO queryUserById(@PathVariable Long id){ User user=userService.getById(id); UserVO userVO=new UserVO(); BeanUtils.copyProperties(user,userVO); return userVO; } @PutMapping("/{id}/deduction/{money}") @ApiOperation("根据id扣减余额") public void updateBalance(@PathVariable Long id,@PathVariable Long money){ userService.deductBalance(id,money); } } service层: @Service public class IUserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService { @Autowired private UserMapper userMapper; @Override public void deductBalance(Long id, Long money) { //1.查询用户 User user=getById(id); if(user==null || user.getStatus()==2){ throw new RuntimeException("用户状态异常!"); } //2.查验余额 if(user.getBalance()<money){ throw new RuntimeException("用户余额不足!"); } //3.扣除余额 update User set balance=balance-money where id=id userMapper.deductBalance(id,money); } } mapper层: @Mapper public interface UserMapper extends BaseMapper<User> { @Update("update user set balance=balance-#{money} where id=#{id}") void deductBalance(Long id, Long money); } 总结:如果是简单查询,如用id来查询、删除,可以直接在Controller层用Iservice方法,否则自定义业务层Service实现具体任务。 Service层的lambdaQuery IService中还提供了Lambda功能来简化我们的复杂查询及更新功能。 相当于「条件构造」和「执行方法」写在一起 this.lambdaQuery() = LambdaQueryWrapper + 内置的执行方法(如 .list()、.one()) 特性 lambdaQuery() lambdaUpdate() 主要用途 构造查询条件,执行 SELECT 操作 构造更新条件,执行 UPDATE(或逻辑删除)操作 支持的方法 .eq(), .like(), .gt(), .orderBy(), .select() 等 .eq(), .lt(), .set(), .setSql() 等 执行方法 .list(), .one(), .page() 等 .update(), .remove()(逻辑删除 **案例一:**实现一个根据复杂条件查询用户的接口,查询条件如下: name:用户名关键字,可以为空 status:用户状态,可以为空 minBalance:最小余额,可以为空 maxBalance:最大余额,可以为空 @GetMapping("/list") @ApiOperation("根据id集合查询用户") public List<UserVO> queryUsers(UserQuery query){ // 1.组织条件 String username = query.getName(); Integer status = query.getStatus(); Integer minBalance = query.getMinBalance(); Integer maxBalance = query.getMaxBalance(); // 2.查询用户 List<User> users = userService.lambdaQuery() .like(username != null, User::getUsername, username) .eq(status != null, User::getStatus, status) .ge(minBalance != null, User::getBalance, minBalance) .le(maxBalance != null, User::getBalance, maxBalance) .list(); // 3.处理vo return BeanUtil.copyToList(users, UserVO.class); } .eq(status != null, User::getStatus, status),使用User::getStatus方法引用并不直接把'Status'插入到 SQL,而是在运行时会被 MyBatis-Plus 解析成实体属性 Status”对应的数据库列是 status。推荐!!! 可以发现lambdaQuery方法中除了可以构建条件,还需要在链式编程的最后添加一个list(),这是在告诉MP我们的调用结果需要是一个list集合。这里不仅可以用list(),可选的方法有: .one():最多1个结果 .list():返回集合结果 .count():返回计数结果 MybatisPlus会根据链式编程的最后一个方法来判断最终的返回结果。 这里不够规范,业务写在controller层中了。 **案例二:**改造根据id修改用户余额的接口,如果扣减后余额为0,则将用户status修改为冻结状态(2) @Override @Transactional public void deductBalance(Long id, Integer money) { // 1.查询用户 User user = getById(id); // 2.校验用户状态 if (user == null || user.getStatus() == 2) { throw new RuntimeException("用户状态异常!"); } // 3.校验余额是否充足 if (user.getBalance() < money) { throw new RuntimeException("用户余额不足!"); } // 4.扣减余额 update tb_user set balance = balance - ? int remainBalance = user.getBalance() - money; lambdaUpdate() .set(User::getBalance, remainBalance) // 更新余额 .set(remainBalance == 0, User::getStatus, 2) // 动态判断,是否更新status .eq(User::getId, id) .eq(User::getBalance, user.getBalance()) // 乐观锁 .update(); } 批量新增 每 batchSize 条记录作为一个 JDBC batch 提交一次(1000 条就一次) @Test void testSaveBatch() { // 准备10万条数据 List<User> list = new ArrayList<>(1000); long b = System.currentTimeMillis(); for (int i = 1; i <= 100000; i++) { list.add(buildUser(i)); // 每1000条批量插入一次 if (i % 1000 == 0) { userService.saveBatch(list); list.clear(); } } long e = System.currentTimeMillis(); System.out.println("耗时:" + (e - b)); } 之所以把 100 000 条记录分成每 1 000 条一批来插,是为了兼顾 性能、内存 和 数据库/JDBC 限制。 JDBC 或数据库参数限制 很多数据库(MySQL、Oracle 等)对单条 SQL 里 VALUES 列表的长度有上限,一次性插入几十万行可能导致 SQL 过长、参数个数过多,被驱动或数据库拒绝。 即使驱动不直接报错,也可能因为网络包(packet)过大而失败。 内存占用和 GC 压力 JDBC 在执行 batch 时,会把所有要执行的 SQL 和参数暂存在客户端内存里。如果一次性缓存 100 000 条记录的参数(可能是几 MB 甚至十几 MB),容易触发 OOM 或者频繁 GC。 事务日志和回滚压力 一次性插入大量数据,数据库需要在事务日志里记录相应条目,回滚时也要一次性回滚所有操作,性能开销巨大。分批能让每次写入都较为“轻量”,回滚范围也更小。 这种本质上是多条单行 INSERT Preparing: INSERT INTO user ( username, password, phone, info, balance, create_time, update_time ) VALUES ( ?, ?, ?, ?, ?, ?, ? ) Parameters: user_1, 123, 18688190001, "", 2000, 2023-07-01, 2023-07-01 Parameters: user_2, 123, 18688190002, "", 2000, 2023-07-01, 2023-07-01 Parameters: user_3, 123, 18688190003, "", 2000, 2023-07-01, 2023-07-01 而如果想要得到最佳性能,最好是将多条SQL合并为一条,像这样: INSERT INTO user ( username, password, phone, info, balance, create_time, update_time ) VALUES (user_1, 123, 18688190001, "", 2000, 2023-07-01, 2023-07-01), (user_2, 123, 18688190002, "", 2000, 2023-07-01, 2023-07-01), (user_3, 123, 18688190003, "", 2000, 2023-07-01, 2023-07-01), (user_4, 123, 18688190004, "", 2000, 2023-07-01, 2023-07-01); 需要修改项目中的application.yml文件,在jdbc的url后面添加参数&rewriteBatchedStatements=true: url: jdbc:mysql://127.0.0.1:3306/mp?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true 但是会存在上述上事务的问题!!! MQ分页 快速入门 1)引入依赖 <!-- 数据库操作:https://mp.baomidou.com/ --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.9</version> </dependency> <!-- MyBatis Plus 分页插件 --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-jsqlparser-4.9</artifactId> </dependency> 2)定义通用分页查询条件实体 @Data @ApiModel(description = "分页查询实体") public class PageQuery { @ApiModelProperty("页码") private Long pageNo; @ApiModelProperty("页码") private Long pageSize; @ApiModelProperty("排序字段") private String sortBy; @ApiModelProperty("是否升序") private Boolean isAsc; } 3)新建一个 UserQuery 类,让它继承自你已有的 PageQuery @Data @ApiModel(description = "用户分页查询实体") public class UserQuery extends PageQuery { @ApiModelProperty("用户名(模糊查询)") private String name; } 4)Service里使用 @Service public class UserService extends ServiceImpl<UserMapper, User> { /** * 用户分页查询(带用户名模糊 + 动态排序) * * @param query 包含 pageNo、pageSize、sortBy、isAsc、name 等字段 */ public Page<User> pageByQuery(UserQuery query) { // 1. 构造 Page 对象 Page<User> page = new Page<>( query.getPageNo(), query.getPageSize() ); // 2. 构造查询条件 LambdaQueryWrapper<User> qw = Wrappers.<User>lambdaQuery() // 当 name 非空时,加上 user_name LIKE '%name%' .like(StrUtil.isNotBlank(query.getName()), User::getUserName, query.getName()); // 3. 动态排序 if (StrUtil.isNotBlank(query.getSortBy())) { String column = StrUtil.toUnderlineCase(query.getSortBy()); boolean asc = Boolean.TRUE.equals(query.getIsAsc()); qw.last("ORDER BY " + column + (asc ? " ASC" : " DESC")); } // 4. 执行分页查询 return this.page(page, qw); } }
后端学习
zy123
5月21日
0
7
0
2025-04-26
微信小程序
微信小程序 转载自黑马程序员。 微信小程序开发 介绍 小程序是一种新的开放能力,开发者可以快速地开发一个小程序。可以在微信内被便捷地获取和传播,同时具有出色的使用体验。 **官方网址:**https://mp.weixin.qq.com/cgi-bin/wx?token=&lang=zh_CN 小程序主要运行微信内部,可通过上述网站来整体了解微信小程序的开发。 **首先,**在进行小程序开发时,需要先去注册一个小程序,在注册的时候,它实际上又分成了不同的注册的主体。我们可以以个人的身份来注册一个小程序,当然,也可以以企业政府、媒体或者其他组织的方式来注册小程序。那么,不同的主体注册小程序,最终开放的权限也是不一样的。比如以个人身份来注册小程序,是无法开通支付权限的。若要提供支付功能,必须是企业、政府或者其它组织等。所以,不同的主体注册小程序后,可开发的功能是不一样的。 **然后,**微信小程序我们提供的一些开发的支持,实际上微信的官方是提供了一系列的工具来帮助开发者快速的接入 并且完成小程序的开发,提供了完善的开发文档,并且专门提供了一个开发者工具,还提供了相应的设计指南,同时也提供了一些小程序体验DEMO,可以快速的体验小程序实现的功能。 **最后,**开发完一个小程序要上线,也给我们提供了详细地接入流程。 准备工作 开发微信小程序之前需要做如下准备工作: 注册小程序 完善小程序信息 下载开发者工具 1). 注册小程序 注册地址:https://mp.weixin.qq.com/wxopen/waregister?action=step1 2). 完善小程序信息 登录小程序后台:https://mp.weixin.qq.com/ 两种登录方式选其一即可 完善小程序信息、小程序类目 查看小程序的 AppID与AppSecret 3). 下载开发者工具 资料中已提供,无需下载,熟悉下载步骤即可。 下载地址: https://developers.weixin.qq.com/miniprogram/dev/devtools/stable.html 扫描登录开发者工具 创建小程序项目 熟悉开发者工具布局 设置不校验合法域名 **注:**开发阶段,小程序发出请求到后端的Tomcat服务器,若不勾选,请求发送失败。 入门案例 实际上,小程序的开发本质上属于前端开发,主要使用JavaScript开发,咱们现在的定位主要还是在后端,所以,对于小程序开发简单了解即可。 小程序目录结构 小程序包含一个描述整体程序的 app 和多个描述各自页面的 page。一个小程序主体部分由三个文件组成,必须放在项目的根目录,如下: 文件说明: **app.js:**必须存在,主要存放小程序的逻辑代码 **app.json:**必须存在,小程序配置文件,主要存放小程序的公共配置 app.wxss: 非必须存在,主要存放小程序公共样式表,类似于前端的CSS样式 对小程序主体三个文件了解后,其实一个小程序又有多个页面。比如说,有商品浏览页面、购物车的页面、订单支付的页面、商品的详情页面等等。那这些页面会放在哪呢? 会存放在pages目录。 每个小程序页面主要由四个文件组成: 文件说明: **js文件:**必须存在,存放页面业务逻辑代码,编写的js代码。 **wxml文件:**必须存在,存放页面结构,主要是做页面布局,页面效果展示的,类似于HTML页面。 **json文件:**非必须,存放页面相关的配置。 **wxss文件:**非必须,存放页面样式表,相当于CSS文件。 编写和编译小程序 1). 编写 进入到index.wxml,编写页面布局 <view class="container"> <view>{{msg}}</view> <view> <button type="default" bindtap="getUserInfo">获取用户信息</button> <image style="width: 100px;height: 100px;" src="{{avatarUrl}}"></image> {{nickName}} </view> <view> <button type="primary" bindtap="wxlogin">微信登录</button> 授权码:{{code}} </view> <view> <button type="warn" bindtap="sendRequest">发送请求</button> 响应结果:{{result}} </view> </view> 进入到index.js,编写业务逻辑代码 Page({ data:{ msg:'hello world', avatarUrl:'', nickName:'', code:'', result:'' }, getUserInfo:function(){ wx.getUserProfile({ desc: '获取用户信息', success:(res) => { console.log(res) this.setData({ avatarUrl:res.userInfo.avatarUrl, nickName:res.userInfo.nickName }) } }) }, wxlogin:function(){ wx.login({ success: (res) => { console.log("授权码:"+res.code) this.setData({ code:res.code }) } }) }, sendRequest:function(){ wx.request({ url: 'http://localhost:8080/user/shop/status', method:'GET', success:(res) => { console.log("响应结果:" + res.data.data) this.setData({ result:res.data.data }) } }) }}) 2). 编译 点击编译按钮 3). 运行效果 点击获取用户信息 点击微信登录 点击发送请求 因为请求http://localhost:8080/user/shop/status,先要启动后台项目。 **注:**设置不校验合法域名,若不勾选,请求发送失败。 发布小程序 小程序的代码都已经开发完毕,要将小程序发布上线,让所有的用户都能使用到这个小程序。 点击上传按钮: 指定版本号: 上传成功: 把代码上传到微信服务器就表示小程序已经发布了吗? **其实并不是。**当前小程序版本只是一个开发版本。 进到微信公众平台,打开版本管理页面。 需提交审核,变成审核版本,审核通过后,进行发布,变成线上版本。 一旦成为线上版本,这就说明小程序就已经发布上线了,微信用户就可以在微信里面去搜索和使用这个小程序了。 微信登录 导入小程序代码 开发微信小程序,本质上是属于前端的开发,我们的重点其实还是后端代码开发。所以,小程序的代码已经提供好了,直接导入到微信开发者工具当中,直接来使用就可以了。 1). 找到资料 2). 导入代码 AppID:使用自己的AppID 3). 查看项目结构 主体的文件:app.js app.json app.wxss 项目的页面比较多,主要存放在pages目录。 4). 修改配置 因为小程序要请求后端服务,需要修改为自己后端服务的ip地址和端口号(默认不需要修改) common-->vendor.js-->搜索(ctrl+f)-->baseUri 微信登录流程 微信登录:https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/login.html 流程图: 步骤分析: 小程序端,调用wx.login()获取code,就是授权码。 小程序端,调用wx.request()发送请求并携带code,请求开发者服务器(自己编写的后端服务)。 开发者服务端,通过HttpClient向微信接口服务发送请求,并携带appId+appsecret+code三个参数。 开发者服务端,接收微信接口服务返回的数据,session_key+opendId等。opendId是微信用户的唯一标识。 开发者服务端,自定义登录态,生成令牌(token)和openid等数据返回给小程序端,方便后绪请求身份校验。 小程序端,收到自定义登录态,存储storage。 小程序端,后绪通过wx.request()发起业务请求时,携带token。 开发者服务端,收到请求后,通过携带的token,解析当前登录用户的id。 开发者服务端,身份校验通过后,继续相关的业务逻辑处理,最终返回业务数据。 接下来,我们使用Postman进行测试。 说明: 调用 wx.login() 获取 临时登录凭证code ,并回传到开发者服务器。 调用 auth.code2Session 接口,换取 用户唯一标识 OpenID 、 用户在微信开放平台帐号下的唯一标识UnionID(若当前小程序已绑定到微信开放平台帐号) 和 会话密钥 session_key。 code是临时的,同一个用户,同一个小程序中,使用不同的code,可以获得唯一的openid! 之后开发者服务器可以根据用户标识来生成自定义登录态,用于后续业务逻辑中前后端交互时识别用户身份。 实现步骤: 1). 获取授权码 点击确定按钮,获取授权码,每个授权码只能使用一次,每次测试,需重新获取。 2). 明确请求接口 请求方式、请求路径、请求参数 3). 发送请求 获取session_key和openid 若出现code been used错误提示,说明授权码已被使用过,请重新获取 需求分析和设计 产品原型 用户进入到小程序的时候,微信授权登录之后才能点餐。需要获取当前微信用户的相关信息,比如昵称、头像等,这样才能够进入到小程序进行下单操作。是基于微信登录来实现小程序的登录功能,没有采用传统账户密码登录的方式。若第一次使用小程序来点餐,就是一个新用户,需要把这个新的用户保存到数据库当中完成自动注册。 登录功能原型图: 业务规则: 基于微信登录实现小程序的登录功能 如果是新用户需要自动完成注册 接口设计 通过微信登录的流程,如果要完成微信登录的话,最终就要获得微信用户的openid。在小程序端获取授权码后,向后端服务发送请求,并携带授权码,这样后端服务在收到授权码后,就可以去请求微信接口服务。最终,后端向小程序返回openid和token等数据。 基于上述的登录流程,就可以设计出该接口的请求参数和返回数据。 **说明:**请求路径/user/user/login,第一个user代表用户端,第二个user代表用户模块。
后端学习
zy123
4月26日
0
10
0
2025-04-17
Maven
Maven Maven仓库分为: 本地仓库:自己计算机上的一个目录(用来存储jar包) 中央仓库:由Maven团队维护的全球唯一的。仓库地址:https://repo1.maven.org/maven2/ 远程仓库(私服):一般由公司团队搭建的私有仓库 POM文件导入依赖的时候,先看本地仓库有没有,没有就看私服,再没有就从中央仓库下载。 Maven创建/导入项目 创建Maven项目 勾选 Create from archetype(可选),也可以选择 maven-archetype-quickstart 等模版。 点击 Next,填写 GAV 坐标 。 GroupId:标识组织或公司(通常使用域名反写,如 com.example) ArtifactId:标识具体项目或模块(如 my-app、spring-boot-starter-web)。 Version:标识版本号(如 1.0-SNAPSHOT、2.7.3) 导入Maven项目 (一)单独的Maven项目 打开 IDEA,在主界面选择 Open(或者在菜单栏选择 File -> Open)。 在文件选择对话框中,定位到已有项目的根目录(包含 pom.xml 的目录)。 选择该目录后,IDEA 会检测到 pom.xml 并询问是否导入为 Maven 项目,点击 OK 或 Import 即可。 IDEA 会自动解析 pom.xml,下载依赖并构建项目结构。 (二)在现有Maven项目中导入独立的Maven项目 在已经打开的 IDEA 窗口中,使用 File -> New -> Module from Existing Sources... 选择待导入项目的根目录(其中包含 pom.xml),IDEA 会将其导入为同一个工程下的另一个模块(Module)。 它们 看起来在一个工程里了,但仍然是两个独立的 Maven 模块。 (三)两个模块属于同一个工程下 可以用一个父pom进行统一管理! 1.新建一个上层目录,如下,MyProject1和MyProject2的内容拷贝过去。 ParentProject/ ├── pom.xml <-- 父模块(聚合模块) ├── MyProject1/ <-- 子模块1 │ └── pom.xml └── MyProject2/ <-- 子模块2 └── pom.xml 2.创建父级pom 父模块 pom.xml 示例: <project> <modelVersion>4.0.0</modelVersion> <groupId>com.example</groupId> <artifactId>ParentProject</artifactId> <version>1.0-SNAPSHOT</version> <packaging>pom</packaging> //必写 <modules> <module>MyProject1</module> //必写 <module>MyProject2</module> </modules> </project> 3.修改子模块 pom.xml ,加上: <parent> <groupId>com.example</groupId> <artifactId>ParentProject</artifactId> <version>1.0-SNAPSHOT</version> <relativePath>../pom.xml</relativePath> <!-- 可省略 --> </parent> 如果子模块中无需与父级不同的配置,可以不写,就自动继承父级配置;若写了同名配置,则表示你想要覆盖或合并父级配置。 4.File -> Open选择父级的pom,会自动导入其下面两个项目。 但是,仅仅这样无法让模块之间产生联动!需要在此基础上进行(四)的操作! (四)通过 Maven 依赖引用 如果你的两个模块之间存在依赖关系(如第一个模块需要使用第二个模块的类)还必须在 MyProject1 的 POM 里显式声明对 MyProject2 的依赖。 MyProject1的pom.xml: <project> <!-- 继承父 POM --> <parent> <groupId>com.example</groupId> <artifactId>ParentProject</artifactId> <version>1.0-SNAPSHOT</version> <relativePath>../pom.xml</relativePath> </parent> <artifactId>MyProject1</artifactId> <packaging>jar</packaging> <dependencies> <!-- 显式依赖于 MyProject2 --> <dependency> <groupId>com.example</groupId> <artifactId>MyProject2</artifactId> <!-- 不写 <version>,Maven 会自动用父 POM 的 version --> </dependency> <!-- 其他依赖… --> </dependencies> </project> 如何打包? 在父 POM 根目录执行 mvn clean package/mvn clean install。 先构建 MyProject2(因为 MyProject1 依赖它) 父 POM 自身不产物,模块的 JAR 都在各自的 target/ 下。 Maven坐标 什么是坐标? Maven中的坐标是 == 资源的唯一标识 == 通过该坐标可以唯一定位资源位置 使用坐标来定义项目或引入项目中需要的依赖 依赖管理 可以到mvn的中央仓库(https://mvnrepository.com/)中搜索获取依赖的坐标信息 <dependencies> <!-- 第1个依赖 : logback --> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.2.11</version> </dependency> <!-- 第2个依赖 : junit --> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> </dependencies> 更改之后可以在界面上看到一个maven刷新按钮,点击一下就开始联网下载依赖了,成功后可以看到 排除依赖 A依赖B,B依赖C,如果A不想将C依赖进来,可以同时排除C,被排除的资源无需指定版本。 <dependency> <groupId>com.itheima</groupId> <artifactId>maven-projectB</artifactId> <version>1.0-SNAPSHOT</version> <!--排除依赖, 主动断开依赖的资源--> <exclusions> <exclusion> <groupId>junit</groupId> <artifactId>junit</artifactId> </exclusion> </exclusions> </dependency> 依赖范围 scope值 主程序 测试程序 打包(运行) 范例 compile(默认) Y Y Y log4j test - Y - junit provided Y Y - servlet-api runtime - Y Y jdbc驱动 注意!!!这里的scope如果是test,那么它的作用范围在src/test/java下,在src/main/java下无法导包! Maven 多模块工程 父 POM 用 <dependencyManagement> 锁版本,子模块按需在 <dependencies> 中声明自己用的依赖。对“真正所有模块都要”的依赖,可以放到父 POM 顶层 <dependencies>,让它们自动继承。 父 POM(pom.xml): <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.example</groupId> <artifactId>parent-project</artifactId> <version>1.0.0</version> <packaging>pom</packaging> <!-- 声明所有子模块 --> <modules> <module>service-a</module> <module>service-b</module> </modules> <!-- 1. 统一锁定版本号(子模块引用时不用写 <version>) --> <dependencyManagement> <dependencies> <!-- Spring Boot Web Starter --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>2.7.3</version> </dependency> <!-- Lombok --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.20</version> </dependency> </dependencies> </dependencyManagement> <!-- 2. 所有模块都需要的“公共依赖”放这里,子模块自动继承 --> <dependencies> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <!-- 不用写 <version>,会从上面 dependencyManagement 拿 --> <scope>provided</scope> </dependency> </dependencies> </project> 子pom <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <!-- 继承父 POM --> <parent> <groupId>com.example</groupId> <artifactId>parent-project</artifactId> <version>1.0.0</version> <relativePath>../pom.xml</relativePath> </parent> <artifactId>service-a</artifactId> <packaging>jar</packaging> <dependencies> <!-- 1. 从父 dependencyManagement 拿版本,不需要写 <version> --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- 2. lombok 已经在父 POM 顶层 dependencies 引入,这里如果要用,也可不再声明 --> </dependencies> </project> 父pom的<packaging>pom</packaging>表示它只是一个 POM 模块,不会产出任何可执行的 JAR。 子pom的<relativePath>../pom.xml</relativePath>告诉 Maven 去哪个相对路径找父 POM 文件 注意:如果子模块A依赖于B模块,那么B模块中的依赖会传递给A,比如B中引入了org.apache.httpcomponents,那么A模块的类中可以直接import这个库。反过来不行!大坑! Maven生命周期 主要关注以下几个: • clean:移除上一次构建生成的文件 (Target文件夹) • compile:编译 src/main/java 中的 Java 源文件至 target/classes • test:使用合适的单元测试框架运行测试(junit) • package:将编译后的文件打包,如:jar、war等 • install:将打包后的产物(如 jar)安装到本地仓库 后面的生命周期执行的时候会自动执行前面所有生命周期! compile: src/ ├── main/ │ ├── java/ │ │ └── com/ │ │ └── example/ │ │ └── App.java │ └── resources/ │ ├── application.yml │ └── static/ │ └── logo.png └── test/ ├── java/ │ └── com/ │ └── example/ │ └── AppTest.java └── resources/ └── test-data.json 映射到 target/ 后: target/ ├── classes/ ← 主代码和资源的输出根目录 │ ├── com/ │ │ └── example/ │ │ └── App.class ← 编译自 src/main/java/com/example/App.java │ ├── application.yml ← 复制自 src/main/resources/application.yml │ └── static/ │ └── logo.png ← 复制自 src/main/resources/static/logo.png └── test-classes/ ← 测试代码和测试资源的输出根目录 ├── com/ │ └── example/ │ └── AppTest.class ← 编译自 src/test/java/com/example/AppTest.java └── test-data.json ← 复制自 src/test/resources/test-data.json test: 扫描 src/test/java 下所有符合默认命名规则的测试类: **/Test*.java **/*Test.java **/*TestCase.java 编译 这些测试类到 target/test-classes。 逐个执行(默认是串行)所有这些编译后的测试类。 package: 打包失败的把test步骤去掉! Maven镜像加速 maven构建依赖可能比较慢,需创建 .mvn/settings.xml (这个提速非常多!务必添加) <settings xmlns="http://maven.apache.org/SETTINGS/1.1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.1.0 https://maven.apache.org/xsd/settings-1.1.0.xsd"> <mirrors> <mirror> <id>aliyun</id> <name>aliyun maven</name> <url>https://maven.aliyun.com/repository/public</url> <mirrorOf>central,apache.snapshots</mirrorOf> </mirror> </mirrors> </settings> 使用阿里云镜像加速,.mvn 放在项目根目录下
后端学习
zy123
4月17日
0
5
0
2025-04-03
JAVA面试题
JAVA面试题(1) 说说 Java 中 HashMap 的原理? 为什么引入红黑树: 当hash冲突较多的时候,链表中的元素会增多,插入、删除、查询的效率会变低,退化成O(n)使用红黑树可以优化插入、删除、查询的效率,logN级别。 转换时机: 链表上的元素个数大于等于8 且 数组长度大于等于64; 链表上的元素个数小于等于6的时候,红黑树退化成链表。 链表插入方式变更:从"头插法"改为"尾插法" 头插法特点: 插入时不需要遍历链表 直接替换头结点 扩容时会导致链表逆序 多线程环境下可能产生死循环 尾插法改进: 避免扩容时的链表逆序 解决多线程环境下的潜在死循环问题 Java 中 ConcurrentHashMap 1.7 和 1.8 之间有哪些区别? ConcurrentHashMap 不同JDK版本的实现对比 数据结构 JDK1.7: 使用 Segment(分段锁) + HashEntry数组 + 链表 的数据结构 JDK1.8及之后: 使用 数组 + 链表/红黑树 的数据结构(与HashMap类似) 锁的类型与宽度 JDK1.7: 分段锁(Segment)继承了 ReentrantLock Segment容量默认16,不会扩容 → 默认支持16个线程并发访问 JDK1.8: 使用 synchronized + CAS 保证线程安全 空节点:通过CAS添加(put操作,多个线程可能同时想要将一个新的键值对插入到同一个桶中,这时它们会使用 CAS 来比较当前桶中的元素(或节点)是否已经被修改过。) 非空节点:通过synchronized加锁,只锁住该桶,其他桶可以并行访问。 渐进式扩容(JDK1.8+) 触发条件:元素数量 ≥ 数组容量 × 负载因子(默认0.75) 扩容过程: 创建2倍大小的新数组 线程操作数据时,逐步迁移旧数组数据到新数组 使用 transferIndex 标记迁移进度 直到旧数组数据完全迁移完成 500. 什么是 Java 的 CAS(Compare-And-Swap)操作? CAS操作包含三个基本操作数: 内存位置(V):要更新的变量 预期原值(A):认为变量当前应该具有的值 新值(B):想要更新为的值 CAS 工作原理: 读取内存位置V的当前值为A 计算新值B 当且仅当V的值等于A时,将V的值设置为B 如果不等于A,则操作失败(通常重试) 伪代码表示: if (V == A) { V = B; return true; } else { return false; }
后端学习
zy123
4月3日
0
4
0
2025-03-21
JavaWeb——后端
JavaWeb——后端 好用的操作 右键文件/文件夹选择Copy Path/Reference,可以获得完整的包路径 Java版本解决方案 单个Java文件运行: Edit Configurations 针对单个运行配置:每个 Java 运行配置(如主类、测试类等)可以独立设置其运行环境(如 JRE 版本、程序参数、环境变量等)。 不影响全局项目:修改某个运行配置的环境不会影响其他运行配置或项目的全局设置。 如何调整全局项目的环境 打开 File -> Project Structure -> Project。 在 Project SDK 中选择全局的 JDK 版本(如 JDK 17)。 在 Project language level 中设置全局的语言级别(如 17)。 Java Compiler File -> Settings -> Build, Execution, Deployment -> Compiler -> Java Compiler Maven Runner File -> Settings -> Build, Execution, Deployment -> Build Tools -> Maven -> Runner 三者之间的关系 全局项目环境 是基准,决定项目的默认 JDK 和语言级别。 Java Compiler 控制编译行为,可以覆盖全局的 Project language level。 Maven Runner 控制 Maven 命令的运行环境,可以覆盖全局的 Project SDK。 Maven 项目: 确保 pom.xml 中的 <maven.compiler.source> 和 <maven.compiler.target> 与 Project SDK 和 Java Compiler 的配置一致。 确保 Maven Runner 中的 JRE 与 Project SDK 一致。 如果还是不行,pom文件右键点击maven->reload project HTTP协议 响应状态码 状态码分类 说明 1xx 响应中 --- 临时状态码。表示请求已经接受,告诉客户端应该继续请求或者如果已经完成则忽略 2xx 成功 --- 表示请求已经被成功接收,处理已完成 3xx 重定向 --- 重定向到其它地方,让客户端再发起一个请求以完成整个处理 4xx 客户端错误 --- 处理发生错误,责任在客户端,如:客户端的请求一个不存在的资源,客户端未被授权,禁止访问等 5xx 服务器端错误 --- 处理发生错误,责任在服务端,如:服务端抛出异常,路由出错,HTTP版本不支持等 状态码 英文描述 解释 200 OK 客户端请求成功,即处理成功,这是我们最想看到的状态码 302 Found 指示所请求的资源已移动到由Location响应头给定的 URL,浏览器会自动重新访问到这个页面 304 Not Modified 告诉客户端,你请求的资源至上次取得后,服务端并未更改,你直接用你本地缓存吧。隐式重定向 400 Bad Request 客户端请求有语法错误,不能被服务器所理解 403 Forbidden 服务器收到请求,但是拒绝提供服务,比如:没有权限访问相关资源 404 Not Found 请求资源不存在,一般是URL输入有误,或者网站资源被删除了 405 Method Not Allowed 请求方式有误,比如应该用GET请求方式的资源,用了POST 429 Too Many Requests 指示用户在给定时间内发送了太多请求(“限速”),配合 Retry-After(多长时间后可以请求)响应头一起使用 500 Internal Server Error 服务器发生不可预期的错误。服务器出异常了,赶紧看日志去吧 503 Service Unavailable 服务器尚未准备好处理请求,服务器刚刚启动,还未初始化好 开发规范 REST风格 在前后端进行交互的时候,我们需要基于当前主流的REST风格的API接口进行交互。 什么是REST风格呢? REST(Representational State Transfer),表述性状态转换,它是一种软件架构风格。 传统URL风格如下: http://localhost:8080/user/getById?id=1 GET:查询id为1的用户 http://localhost:8080/user/saveUser POST:新增用户 http://localhost:8080/user/updateUser PUT:修改用户 http://localhost:8080/user/deleteUser?id=1 DELETE:删除id为1的用户 我们看到,原始的传统URL,定义比较复杂,而且将资源的访问行为对外暴露出来了。 基于REST风格URL如下: http://localhost:8080/users/1 GET:查询id为1的用户 http://localhost:8080/users POST:新增用户 http://localhost:8080/users PUT:修改用户 http://localhost:8080/users/1 DELETE:删除id为1的用户 其中总结起来,就一句话:通过URL定位要操作的资源,通过HTTP动词(请求方式)来描述具体的操作。 REST风格后端代码: @RestController @RequestMapping("/depts") //定义当前控制器的请求前缀 public class DeptController { // GET: 查询资源 @GetMapping("/{id}") public Dept getDept(@PathVariable Long id) { ... } // POST: 新增资源 @PostMapping public void createDept(@RequestBody Dept dept) { ... } // PUT: 更新资源 @PutMapping public void updateDept(@RequestBody Dept dept) { ... } // DELETE: 删除资源 @DeleteMapping("/{id}") public void deleteDept(@PathVariable Long id) { ... } } GET:查询,用 URL 传参,不能带 body。 POST:创建/提交,可以用 body 传数据(JSON、表单)。 PUT:更新,可以用 body 。 DELETE:删除,一般无 body,只要 -X DELETE。 开发流程 查看页面原型明确需求 根据页面原型和需求,进行表结构设计、编写接口文档(已提供) 阅读接口文档 思路分析 功能接口开发 就是开发后台的业务功能,一个业务功能,我们称为一个接口(Controller 中一个完整的处理请求的方法) 功能接口测试 功能开发完毕后,先通过Postman进行功能接口测试,测试通过后,再和前端进行联调测试 前后端联调测试 和前端开发人员开发好的前端工程一起测试 SpringBoot Servlet 容器 是用于管理和运行 Web 应用的环境,它负责加载、实例化和管理 Servlet 组件,处理 HTTP 请求并将请求分发给对应的 Servlet。常见的 Servlet 容器包括 Tomcat、Jetty、Undertow 等。 SpringBoot的WEB默认内嵌了tomcat服务器,非常方便!!! 浏览器与 Tomcat 之间通过 HTTP 协议进行通信,而 Tomcat 则充当了中间的桥梁,将请求路由到你的 Java 代码,并最终将处理结果返回给浏览器。 查看springboot版本:查看pom文件 <parent> <artifactId>spring-boot-starter-parent</artifactId> <groupId>org.springframework.boot</groupId> <version>2.7.3</version> </parent> 版本为2.7.3 快速启动 新建spring initializr project 删除以下文件 新建HelloController类 package edu.whut.controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class HelloController { @RequestMapping("/hello") public String hello(){ System.out.println("hello"); return "hello"; } } 然后启动服务器,main程序 package edu.whut; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class SprintbootQuickstartApplication { public static void main(String[] args) { SpringApplication.run(SprintbootQuickstartApplication.class, args); } } 然后浏览器访问 localhost:8080/hello。 SpringBoot请求 简单参数 在Springboot的环境中,对原始的API进行了封装,接收参数的形式更加简单。 如果是简单参数,参数名与形参变量名相同,定义同名的形参即可接收参数。 @RestController public class RequestController { // http://localhost:8080/simpleParam?name=Tom&age=10 // 第1个请求参数: name=Tom 参数名:name,参数值:Tom // 第2个请求参数: age=10 参数名:age , 参数值:10 //springboot方式 @RequestMapping("/simpleParam") public String simpleParam(String name , Integer age ){//形参名和请求参数名保持一致 System.out.println(name+" : "+age); return "OK"; } } 如果方法形参名称与请求参数名称不一致,controller方法中的形参还能接收到请求参数值吗? 解决方案:可以使用Spring提供的@RequestParam注解完成映射 在方法形参前面加上 @RequestParam 然后通过value属性执行请求参数名,从而完成映射。代码如下: @RestController public class RequestController { // http://localhost:8080/simpleParam?name=Tom&age=20 // 请求参数名:name //springboot方式 @RequestMapping("/simpleParam") public String simpleParam(@RequestParam("name") String username , Integer age ){ System.out.println(username+" : "+age); return "OK"; } } 实体参数 复杂实体对象指的是,在实体类中有一个或多个属性,也是实体对象类型的。如下: User类中有一个Address类型的属性(Address是一个实体类) 复杂实体对象的封装,需要遵守如下规则: 请求参数名与形参对象属性名相同,按照对象层次结构关系即可接收嵌套实体类属性参数。 注意:这里User前面不能加@RequestBody是因为请求方式是 Query 或 路径 参数;如果是JSON请求体(Body)就必须加。 @RequestMapping("/complexpojo") public String complexpojo(User user){ System.out.println(user); return "OK"; } @Data @NoArgsConstructor @AllArgsConstructor public class User { private String name; private Integer age; private Address address; } @Data @NoArgsConstructor @AllArgsConstructor public class Address { private String province; private String city; } 数组参数 数组参数:请求参数名与形参数组名称相同且请求参数为多个,定义数组类型形参即可接收参数 @RestController public class RequestController { //数组集合参数 @RequestMapping("/arrayParam") public String arrayParam(String[] hobby){ System.out.println(Arrays.toString(hobby)); return "OK"; } } 路径参数 请求的URL中传递的参数 称为路径参数。例如: http://localhost:8080/user/1 http://localhost:880/user/1/0 注意,路径参数使用大括号 {} 定义 @RestController public class RequestController { //路径参数 @RequestMapping("/path/{id}/{name}") public String pathParam2(@PathVariable Integer id, @PathVariable String name){ System.out.println(id+ " : " +name); return "OK"; } } 在路由定义里用 {id} 只是一个占位符,实际请求时 不要 带大括号 JSON格式参数 { "backtime": [ "与中标人签订合同后 5日内", "投标截止时间前撤回投标文件并书面通知招标人的,2日内", "开标现场投标文件被拒收,开标结束后,2日内" ], "employees": [ { "firstName": "John", "lastName": "Doe" }, { "firstName": "Anna", "lastName": "Smith" }, { "firstName": "Peter", "lastName": "Jones" } ] } JSON 格式的核心特征 接口文档中的请求参数中是 'Body' 发送数据 数据为键值对:数据存储在键值对中,键和值用冒号分隔。在你的示例中,每个对象有两个键值对,如 "firstName": "John"。 使用大括号表示对象:JSON 使用大括号 {} 包围对象,对象可以包含多个键值对。 使用方括号表示数组:JSON 使用方括号 [] 表示数组,数组中可以包含多个值,包括数字、字符串、对象等。在该示例中:"employees" 是一个对象数组,数组中的每个元素都是一个对象。 Postman如何发送JSON格式数据: 服务端Controller方法如何接收JSON格式数据: 传递json格式的参数,在Controller中会使用实体类进行封装。 封装规则:JSON数据键名与形参对象属性名相同,定义POJO类型形参即可接收参数。需要使用 @RequestBody标识。 @Data @NoArgsConstructor @AllArgsConstructor public class DataDTO { private List<String> backtime; private List<Employee> employees; } @Data @NoArgsConstructor @AllArgsConstructor public class Employee { private String firstName; private String lastName; } @RestController public class DataController { @PostMapping("/data") public String receiveData(@RequestBody DataDTO data) { System.out.println("Backtime: " + data.getBacktime()); System.out.println("Employees: " + data.getEmployees()); return "OK"; } } JSON格式工具包 用于高效地进行 JSON 与 Java 对象之间的序列化和反序列化操作。 引入依赖: <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.76</version> </dependency> 使用: import com.alibaba.fastjson.JSON; public class FastJsonDemo { public static void main(String[] args) { // 创建一个对象 User user = new User("Alice", 30); // 对象转 JSON 字符串 String jsonString = JSON.toJSONString(user); System.out.println("JSON String: " + jsonString); // JSON 字符串转对象 User parsedUser = JSON.parseObject(jsonString, User.class); System.out.println("Parsed User: " + parsedUser); } } // JSON String: {"age":30,"name":"Alice"} // Parsed User: User(name=Alice, age=30) SpringBoot响应 @ResponseBody注解: 位置:书写在Controller方法上或类上 作用:将方法返回值直接响应给浏览器 如果返回值类型是实体对象/集合,将会转换为JSON格式后在响应给浏览器 @RestController = @Controller + @ResponseBody 统一响应结果: 下图返回值分别是字符串、对象、集合。 定义统一返回结果类 响应状态码:当前请求是成功,还是失败 状态码信息:给页面的提示信息 返回的数据:给前端响应的数据(字符串、对象、集合) 定义在一个实体类Result来包含以上信息。代码如下: @Data @NoArgsConstructor @AllArgsConstructor public class Result { private Integer code;//响应码,1 代表成功; 0 代表失败 private String msg; //响应信息 描述字符串 private Object data; //返回的数据 //增删改 成功响应 public static Result success(){ return new Result(1,"success",null); } //查询 成功响应 public static Result success(Object data){ return new Result(1,"success",data); } //失败响应 public static Result error(String msg){ return new Result(0,msg,null); } } Spring分层架构 三层架构 Controller层接收请求,调用Service层;Service层先调用Dao层获取数据,然后实现自己的业务逻辑处理部分,最后返回给Controller层;Controller层再响应数据。可理解为递归的过程。 传统模式:对象的创建、管理和依赖关系都由程序员手动编写代码完成,程序内部控制对象的生命周期。 例如: public class A { private B b; public A() { b = new B(); // A 自己创建并管理 B 的实例 } } 假设有类 A 依赖类 B,在传统方式中,类 A 可能在构造方法或方法内部直接调用 new B() 来创建 B 的实例。 如果 B 的创建方式发生变化,A 也需要修改代码。这就导致了耦合度较高。 软件设计原则:高内聚低耦合。 高内聚指的是:一个模块中各个元素之间的联系的紧密程度,如果各个元素(语句、程序段)之间的联系程度越高,则内聚性越高,即 "高内聚"。 低耦合指的是:软件中各个层、模块之间的依赖关联程序越低越好。 IOC&DI 分层解耦 外部容器(例如 Spring 容器)是一个负责管理对象创建、配置和生命周期的软件系统。 它扫描项目中的类,根据预先配置或注解,将这些类实例化为 Bean。 它维护各个 Bean 之间的依赖关系,并在创建 Bean 时把它们所需的依赖“注入”进去。 依赖注入(DI):类 A 不再自己创建 B,而是声明自己需要一个 B,容器在创建 A 时会自动将 B 的实例提供给 A。 public class A { private B b; // 通过构造器注入依赖 public A(B b) { this.b = b; } } Bean 对象:在 Spring 中,被容器管理的对象称为 Bean。通过注解(如 @Component, @Service, @Repository, @Controller),可以将一个普通的 Java 类声明为 Bean,容器会负责它的创建、初始化以及生命周期管理。 任务:完成Controller层、Service层、Dao层的代码解耦 思路: 删除Controller层、Service层中new对象的代码 Service层及Dao层的实现类,交给IOC容器管理 为Controller及Service注入运行时依赖的对象 Controller程序中注入依赖的Service层对象 Service程序中注入依赖的Dao层对象 第1步:删除Controller层、Service层中new对象的代码 第2步:Service层及Dao层的实现类,交给IOC容器管理 使用Spring提供的注解:@Component ,就可以实现类交给IOC容器管理 第3步:为Controller及Service注入运行时依赖的对象 使用Spring提供的注解:@Autowired ,就可以实现程序运行时IOC容器自动注入需要的依赖对象 **如果我有多个实现类,eg:EmpServiceA、EmpServiceB,我该如何切换呢?**两种方法 只需在需要使用的实现类上加@Component,注释掉不需要用到的类上的@Component。可以把@Component想象成装入盒子,@Autowired想象成拿出来,因此只需改变放入的物品,而不需改变拿出来的这个动作。 // 只启用 EmpServiceA,其他实现类的 @Component 注解被注释或移除 @Component public class EmpServiceA implements EmpService { // 实现细节... } // EmpServiceB 没有被 Spring 管理 // @Component // public class EmpServiceB implements EmpService { ... } 在@Component上面加上@Primary,表明该类优先生效 // 默认使用 EmpServiceB,其他实现类也在容器中,但未标记为 Primary @Component public class EmpServiceA implements EmpService { // 实现细节... } @Component @Primary // 默认优先注入 public class EmpServiceB implements EmpService { // 实现细节... } Component衍生注解 注解 说明 位置 @Controller @Component的衍生注解 标注在控制器类上Controller @Service @Component的衍生注解 标注在业务类上Service @Repository @Component的衍生注解 标注在数据访问类上(由于与mybatis整合,用的少)DAO @Component 声明bean的基础注解 不属于以上三类时,用此注解 注:@Mapper 注解本身并不是 Spring 框架提供的,是用于 MyBatis 数据层的接口标识,但效果类似。 SpringBoot原理 容器启动 在 Spring 框架中,“容器启动”指的是 ApplicationContext 初始化过程,主要包括配置解析、加载 Bean 定义、实例化和初始化 Bean 以及完成依赖注入。具体来说,容器启动的时机包括以下几个关键点: 当你启动一个 Spring 应用时,无论是通过直接运行一个包含 main 方法的类,还是部署到一个 Servlet 容器中,Spring 的应用上下文都会被创建和初始化。这个过程包括: 读取配置:加载配置文件或注解中指定的信息,确定哪些组件由 Spring 管理。 注册 Bean 定义:将所有扫描到的 Bean 定义注册到容器中。 实例化 Bean:根据 Bean 定义创建实例。默认情况下,所有单例 Bean在启动时被创建(除非配置为懒加载)。 依赖注入:解析 Bean 之间的依赖关系,并自动注入相应的依赖。 依赖注入 1.Autowird注入 @Autowired private PaymentClient paymentClient; // 字段直接加 @Autowired 2.构造器注入(推荐) 1)手写构造器 public class OrderService { private final PaymentClient paymentClient; // 在构造器上无需加 @Autowired(Spring Boot 下可省略) public OrderService(PaymentClient paymentClient) { this.paymentClient = paymentClient; } } 2)Lombok @RequiredArgsConstructor 用 Lombok 自动为所有 final 字段生成构造器,进一步简化写法: @RequiredArgsConstructor public class OrderService { private final PaymentClient paymentClient; // Lombok 会在编译期生成构造器 } controller层应注入接口类,而不是子类,如果只有一个子类实现类,那么直接注入即可,否则需要指定注入哪一个 @Service("categoryServiceImplV1") public class CategoryServiceImplV1 implements CategoryService { … } @Service("categoryServiceImplV2") public class CategoryServiceImplV2 implements CategoryService { … } @RestController @RequiredArgsConstructor // 推荐构造器注入 public class CategoryController { @Qualifier("categoryServiceImplV2") // 指定注入 V2 private final CategoryService categoryService; } 配置文件 配置优先级 在SpringBoot项目当中,常见的属性配置方式有5种, 3种配置文件,加上2种外部属性的配置(Java系统属性、命令行参数)。优先级(从低到高): application.yaml(忽略) application.yml application.properties java系统属性(-Dxxx=xxx) 命令行参数(--xxx=xxx) 在 Spring Boot 项目中,通常使用的是 application.yml 或 application.properties 文件,这些文件通常放在项目的 src/main/resources 目录下。 如果项目已经打包上线了,这个时候我们又如何来设置Java系统属性和命令行参数呢? java -Dserver.port=9000 -jar XXXXX.jar --server.port=10010 在这个例子中,由于命令行参数的优先级高于 Java 系统属性,最终生效的 server.port 是 10010。 properties 位置:src/main/resources/application.properties 将配置信息写在application.properties,用注解@Value获取配置文件中的数据 yml配置文件(推荐!!!) 位置:src/main/resources/application.yml 了解下yml配置文件的基本语法: 大小写敏感 数据前边必须有空格,作为分隔符 使用缩进表示层级关系,缩进时,不允许使用Tab键,只能用空格(idea中会自动将Tab转换为空格) 缩进的空格数目不重要,只要相同层级的元素左侧对齐即可 #表示注释,从这个字符一直到行尾,都会被解析器忽略 对象/map集合 user: name: zhangsan detail: age: 18 password: "123456" 数组/List/Set集合 hobby: - java - game - sport //获取示例 @Value("${hobby}") private List<String> hobby; 以上获取配置文件中的属性值,需要通过@Value注解,有时过于繁琐!!! @ConfigurationProperties 是用来将外部配置(如 application.yml)映射到一个 POJO 上的。 在 Spring Boot 中,根据 驼峰命名转换规则,自动将 YAML 配置文件中的 键名(例如 user-token-name user_token_name)映射到 Java 类中的属性(例如 userTokenName)。 @Data @Component @ConfigurationProperties(prefix = "aliyun.oss") public class AliOssProperties { private String endpoint; private String accessKeyId; private String accessKeySecret; private String bucketName; } Spring提供的简化方式套路: 需要创建一个实现类,且实体类中的属性名和配置文件当中key的名字必须要一致 比如:配置文件当中叫endpoints,实体类当中的属性也得叫endpoints,另外实体类当中的属性还需要提供 getter / setter方法 ==》@Data 需要将实体类交给Spring的IOC容器管理,成为IOC容器当中的bean对象 ==>@Component 在实体类上添加@ConfigurationProperties注解,并通过perfix属性来指定配置参数项的前缀 (可选)引入依赖pom.xml (自动生成配置元数据,让 IDE 能识别并补全你在 application.properties/yml 中的自定义配置项,提高开发体验,不加不影响运行!) <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> </dependency> 隐私数据配置 通常设置一个 application-local.yml 存放隐私数据,且加入 .gitignore ,表示仅存储在本地;然后application.yml 通过占位符引入该文件,eg: application.yml # 主配置文件,导入本地隐私文件,并通过占位符引用 spring: config: # Spring Boot 2.4+ 推荐用 import import: optional:classpath:/application-local.yml myapp: datasource: url: jdbc:mysql://localhost:3306/pay_mall # 下面两个会从 application-local.yml 里拿 username: ${datasource.username} password: ${datasource.password} application-local.yml # 本地专属配置,激活时才会加载 datasource: username: root password: 123456 这里有个松散绑定的原则,对于 ${datasource.username} 这里匹配有效: userName username user-name user_name 这四个是等价的 底层做法是: 全部字符转小写 → username 去掉分隔符(-、_、.、空格) → username 再匹配到 Java Bean 里的驼峰字段 userName(或直接 username,视你写的字段而定) Bean 的获取和管理 获取Bean 1.自动装配(@Autowired) @Service public class MyService { @Autowired private MyRepository myRepository; // 自动注入 MyRepository Bean } 2.手动获取(ApplicationContext) @Autowired 自动将 Spring 创建的 ApplicationContext 注入到 applicationContext 字段中, 再通过 applicationContext.getBean(...) 拿到其他 Bean Spring 会默认采用类名并将首字母小写作为 Bean 的名称。例如,类名为 DeptController 的组件默认名称就是 deptController。 @RunWith(SpringRunner.class) @SpringBootTest public class SpringbootWebConfig2ApplicationTests { @Autowired private ApplicationContext applicationContext; // IoC 容器 @Test public void testGetBean() { // 根据 Bean 名称获取 DeptController bean = (DeptController) applicationContext.getBean("deptController"); System.out.println(bean); } } 默认情况下,Spring 在容器启动时会创建所有单例 Bean(饿汉模式);使用 @Lazy 注解则可实现延迟加载(懒汉模式) bean的作用域 作用域 说明 singleton 容器内同名称的bean只有一个实例(单例)(默认) prototype 每次使用该bean时会创建新的实例(非单例) 在设计单例类时,通常要求它们是无状态的,不仅要确保成员变量不可变,还需要确保成员方法不会对共享的、可变的状态进行不受控制的修改,从而实现整体的线程安全。 @Service public class CalculationService { // 不可变的成员变量 private final double factor = 2.0; // 成员方法仅依赖方法参数和不可变成员变量 public double multiply(double value) { return value * factor; } } 更改作用域方法: 在bean类上加注解@Scope("prototype")(或其他作用域标识)即可。 第三方 Bean配置 如果要管理的bean对象来自于第三方(不是自定义的),是无法用@Component 及衍生注解声明bean的,就需要用到**@Bean**注解。 如果需要定义第三方Bean时, 通常会单独定义一个配置类 @Configuration // 配置类 public class CommonConfig { // 定义第三方 Bean,并交给 IoC 容器管理 @Bean public SAXReader reader(DeptService deptService) { System.out.println(deptService); return new SAXReader(); } } 在应用启动时,Spring 会调用配置类中标注 @Bean 的方法,将方法返回值注册为容器中的 Bean 对象。 默认情况下,该 Bean 的名称就是该方法的名字。本例 Bean 名称默认就是 "reader"。Bean的类型就是返回值的类型,这里是SAXReader。 使用: @Service public class XmlProcessingService { // 按类型注入 @Autowired private SAXReader reader; //方法的名字!! public void parse(String xmlPath) throws DocumentException { Document doc = reader.read(new File(xmlPath)); // ... 处理 Document ... } } SpirngBoot原理 如果我们直接基于Spring框架进行项目的开发,会比较繁琐。SpringBoot框架之所以使用起来更简单更快捷,是因为SpringBoot框架底层提供了两个非常重要的功能:一个是起步依赖,一个是自动配置。 起步依赖 Spring Boot 只需要引入一个起步依赖(例如 springboot-starter-web)就能满足项目开发需求。这是因为: Maven 依赖传递: 起步依赖内部已经包含了开发所需的常见依赖(如 JSON 解析、Web、WebMVC、Tomcat 等),无需开发者手动引入其它依赖。 结论: 起步依赖的核心原理就是 Maven 的依赖传递机制。 自动配置 Spring Boot 会自动扫描启动类所在包及其子包中的所有带有组件注解(如 @Component, @Service, @Repository, @Controller, @Mapper 等)的类并加载到IOC容器中。 自动配置原理源码入口就是@SpringBootApplication注解,在这个注解中封装了3个注解,分别是: @SpringBootConfiguration 声明当前类是一个配置类,等价于 @Configuration又与之区分 @ComponentScan 进行组件扫描。如果你的项目有server pojo common模块,启动类在com.your.package.server下,那么只会默认扫描com.your.package及其子包。 @ComponentScan({"com.your.package.server", "com.your.package.common"})可以显示指定扫描的包路径。 @EnableAutoConfiguration(自动配置核心注解,下节详解) 自动配置的效果: 在IOC容器中除了我们自己定义的bean以外,还有很多配置类,这些配置类都是SpringBoot在启动的时候加载进来的配置类。这些配置类加载进来之后,它也会生成很多的bean对象。 当我们想要使用这些配置类中生成的bean对象时,可以使用@Autowired就自动注入了。 如何让第三方bean以及配置类生效? 如果配置类(如 CommonConfig)不在 Spring Boot 启动类的扫描路径内(即不在启动类所在包或其子包下): 1.@ComponentScan添加包扫描路径,适合批量导入(繁琐、性能低) 2.通过 @Import 手动导入该配置类。适合精确导入,如: com └── example └── SpringBootApplication.java // 启动类 src └── com └── config └── CommonConfig.java // 配置类 借助 @Import 注解,我们可以将外部的普通类、配置类或实现了 ImportSelector 的类显式导入到 Spring 容器中。 也就是这些类会加载到IOC容器中。 1.使用@Import导入普通类: 如果某个普通类(如 TokenParser)没有 @Component 注解标识,也可以通过 @Import 导入它,使其成为 Spring 管理的 Bean。 // TokenParser 类没有 @Component 注解 public class TokenParser { public void parse(){ System.out.println("TokenParser ... parse ..."); } } 在启动类上使用 @Import 导入: @Import(TokenParser.class) //导入的类会被Spring加载到IOC容器中 @SpringBootApplication public class SpringbootWebConfig2Application { public static void main(String[] args) { SpringApplication.run(SpringbootWebConfig2Application.class, args); } } 2.使用@Import导入配置类: 配置类中可以定义多个 Bean,通过 @Configuration 和 @Bean 注解实现集中管理。 @Configuration public class HeaderConfig { @Bean public HeaderParser headerParser(){ return new HeaderParser(); } @Bean public HeaderGenerator headerGenerator(){ return new HeaderGenerator(); } } 启动类导入配置类: @Import(HeaderConfig.class) //导入配置类 @SpringBootApplication public class SpringbootWebConfig2Application { public static void main(String[] args) { SpringApplication.run(SpringbootWebConfig2Application.class, args); } } 3.使用第三方依赖@EnableXxxx 注解 如果第三方依赖没有提供自动配置支持, 常见方案是第三方依赖提供一个 @EnableXxxx 注解,这个注解内部封装了 @Import,通过它可以一次性导入多个配置或 Bean。 @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Import(MyImportSelector.class)//指定要导入哪些bean对象或配置类 public @interface EnableHeaderConfig { } 在应用启动类上添加第三方依赖提供的 @EnableHeaderConfig 注解,即可导入相关的配置和 Bean。 @EnableHeaderConfig //使用第三方依赖提供的Enable开头的注解 @SpringBootApplication public class SpringbootWebConfig2Application { public static void main(String[] args) { SpringApplication.run(SpringbootWebConfig2Application.class, args); } } 推荐第三种方式! @EnableAutoConfiguration 导入自动配置类 通过元注解 @Import(AutoConfigurationImportSelector.class),在启动时读取所有 JAR 包中 META‑INF/spring.factories 下 EnableAutoConfiguration 对应的自动配置类列表。 将这些配置类当作 @Configuration 导入到 Spring 容器中。 按条件注册 Bean 自动配置类内部使用多种条件注解(如 @ConditionalOnClass、@ConditionalOnMissingBean、@ConditionalOnProperty 等)。 Spring Boot 会检查当前类路径、配置属性和已有 Bean,仅在满足所有条件时,才执行对应的 @Bean 方法,将组件注入 IOC 容器。 @ComponentScan 用于发现和加载应用自身的组件; @EnableAutoConfiguration 则负责加载 Spring Boot 提供的“开箱即用”配置。如: DataSourceAutoConfiguration 检测到常见的 JDBC 驱动(如 HikariCP、Tomcat JDBC)和配置属性(spring.datasource.*)时,自动创建并配置 DataSource。、 WebMvcAutoConfiguration自动配置 Spring MVC 的核心组件,并启用默认的静态资源映射、消息转换器(Jackson JSON)等。但遇到用户自定义的 MVC 支持配置(如继承 WebMvcConfigurationSupport )时会“失效”(Back Off)因为其内部有个注解:@ConditionalOnMissingBean(WebMvcConfigurationSupport.class),一旦容器内有xx类型注解,默认配置自动失效。 常见的注解!! @RequestMapping("/jsonParam"):可以用于控制器级别,也可以用于方法级别。 用于方法:HTTP 请求路径为 /jsonParam 的请求将调用该方法。 @RequestMapping("/jsonParam") public String jsonParam(@RequestBody User user){ System.out.println(user); return "OK"; } 用于控制器: 所有方法的映射路径都会以这个前缀开始。 @RestController @RequestMapping("/depts") public class DeptController { @GetMapping("/{id}") public Dept getDept(@PathVariable Long id) { // 实现获取部门逻辑 } @PostMapping public void createDept(@RequestBody Dept dept) { // 实现新增部门逻辑 } } @RequestBody:这是一个方法参数级别的注解,用于告诉Spring框架将请求体的内容解析为指定的Java对象。 @RestController:这是一个类级别的注解,它告诉Spring框架这个类是一个控制器(Controller),并且处理HTTP请求并返回响应数据。与 @Controller 注解相比,@RestController 注解还会自动将控制器方法返回的数据转换为 JSON 格式,并写入到HTTP响应中,得益于@ResponseBody 。 @RestController = @Controller + @ResponseBody @PathVariable 注解用于将路径参数 {id} 的值绑定到方法的参数 id 上。当请求的路径是 "/path/123" 时,@PathVariable 会将路径中的 "123" 值绑定到方法的参数 id 上。 public String pathParam(@PathVariable Integer id) { System.out.println(id); return "OK"; } //参数名与路径名不同 @GetMapping("/{id}") public ResponseEntity<User> getUserById(@PathVariable("id") Long userId) { } @RequestParam, 1)如果方法的参数名与请求参数名不同,需要在 @RequestParam 注解中指定请求参数的名字。 类似@PathVariable,可以指定参数名称。 @RequestMapping("/example") public String exampleMethod(@RequestParam String name, @RequestParam("age") int userAge) { // 在方法内部使用获取到的参数值进行处理 System.out.println("Name: " + name); System.out.println("Age: " + userAge); return "OK"; } 2)还可以设置默认值 @RequestMapping("/greet") public String greet(@RequestParam(defaultValue = "Guest") String name) { return "Hello, " + name; } 3)如果既改请求参数名字,又要设置默认值 @RequestMapping("/greet") public String greet(@RequestParam(value = "age", defaultValue = "25") int userAge) { return "Age: " + userAge; } 4)如果方法参数是简单类型(int/Integer、String、boolean/Boolean 等及它们的一维数组),那么无需使用@RequestParam,如果是Collection集合类型,必须使用。 List<ItemDTO> queryItemByIds(@RequestParam("ids") Collection<Long> ids); 控制反转与依赖注入: @Component、@Service、@Repository 用于标识 bean 并让容器管理它们,从而实现 IoC。 @Autowired、@Configuration、@Bean 用于实现 DI,通过容器自动装配或配置 bean 的依赖。 数据库相关。 @Mapper注解:表示是mybatis中的Mapper接口,程序运行时,框架会自动生成接口的实现类对象(代理对象),并交给Spring的IOC容器管理 @Select注解:代表的就是select查询,用于书写select查询语句 @SpringBootTest:它会启动 Spring 应用程序上下文,并在测试期间模拟运行整个 Spring Boot 应用程序。这意味着你可以在集成测试中使用 Spring 的各种功能,例如自动装配、依赖注入、配置加载等。 lombok的相关注解。非常实用的工具库。 在pom.xml文件中引入依赖 <!-- 在springboot的父工程中,已经集成了lombok并指定了版本号,故当前引入依赖时不需要指定version --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> 在实体类上添加以下注解(加粗为常用) 注解 作用 @Getter/@Setter 为所有的属性提供get/set方法 @ToString 会给类自动生成易阅读的 toString 方法 @EqualsAndHashCode 根据类所拥有的非静态字段自动重写 equals 方法和 hashCode 方法 @Data 提供了更综合的生成代码功能(@Getter + @Setter + @ToString + @EqualsAndHashCode) @NoArgsConstructor 为实体类生成无参的构造器方法 @AllArgsConstructor 为实体类生成除了static修饰的字段之外带有各参数的构造器方法。 @Slf4j 可以log.info("输出日志信息"); //equals 方法用于比较两个对象的内容是否相同 Address addr1 = new Address("SomeProvince", "SomeCity"); Address addr2 = new Address("SomeProvince", "SomeCity"); System.out.println(addr1.equals(addr2)); // 输出 true log: log.info("应用启动成功"); Long empId = 12L; log.info("当前员工id:{}", empId); //带占位符,推荐! log.info("当前员工id:" + empId); //不错,但不推荐 log.info("当前员工id:", empId); //错误的! @Test,Junit测试单元,可在测试类中定义测试函数,一次性执行所有@Test注解下的函数,不用写main方法 @Override,当一个方法在子类中覆盖(重写)了父类中的同名方法时,为了确保正确性,可以使用 @Override 注解来标记这个方法,这样编译器就能够帮助检查是否正确地重写了父类的方法。 @DateTimeFormat将日期转化为指定的格式。Spring会尝试将接收到的字符串参数转换为控制器方法参数的相应类型。 @RestController public class DateController { // 例如:请求 URL 为 /search?begin=2025-03-28 @GetMapping("/search") public String search(@RequestParam("begin") @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate begin) { // 此时 begin 已经是 LocalDate 类型,可以直接使用 return "接收到的日期是: " + begin; } } @RestControllerAdvice= @ControllerAdvice + @ResponseBody。加上这个注解就代表我们定义了一个全局异常处理器,而且处理异常的方法返回值会转换为json后再响应给前端 @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(Exception.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public String handleException(Exception ex) { // 返回错误提示或错误详情 return "系统发生异常:" + ex.getMessage(); } } @Configuration和@Bean配合使用,可以对第三方bean进行集中的配置管理,依赖注入!!@Bean用于方法上。加了@Configuration,当Spring Boot应用启动时,它会执行一系列的自动配置步骤。 @ComponentScan指定了Spring应该在哪些包下搜索带有@Component、@Service、@Repository、@Controller等注解的类,以便将这些类自动注册为Spring容器管理的Bean.@SpringBootApplication它是一个便利的注解,组合了@Configuration、@EnableAutoConfiguration和@ComponentScan注解。 @Async 注解,异步执行 1.在你的配置类或主启动类上添加: @Configuration @EnableAsync public class AsyncConfig { // 可以自定义线程池 Bean(可选) } 2.在你希望异步执行的方法或它所在的 Bean 上,添加 @Async @Service public class EmailService { @Async public void sendWelcomeEmail(String userId) { // 这个方法会在独立线程中执行 // 调用线程会立即返回,不会等待方法内部逻辑完成 // … 发送邮件的耗时操作 … } } 登录校验 会话技术 会话是和浏览器关联的,当有三个浏览器客户端和服务器建立了连接时,就会有三个会话。同一个浏览器在未关闭之前请求了多次服务器,这多次请求是属于同一个会话。比如:1、2、3这三个请求都是属于同一个会话。当我们关闭浏览器之后,这次会话就结束了。而如果我们是直接把web服务器关了,那么所有的会话就都结束了。 会话跟踪技术有三种: Cookie(客户端会话跟踪技术) Session(服务端会话跟踪技术) 令牌技术 Cookie 原理:会话数据存储在客户端浏览器中,通过浏览器自动管理。 优点:HTTP协议中支持的技术(像Set-Cookie 响应头的解析以及 Cookie 请求头数据的携带,都是浏览器自动进行的,是无需我们手动操作的) 缺点: 移动端APP(Android、IOS)中无法使用Cookie 不安全,用户可以自己禁用Cookie Cookie不能跨域传递 Session 原理:服务端存储会话数据(如内存、Redis),客户端只保存会话 ID。 第一次请求 浏览器没有 JSESSIONID Cookie,服务端看到没有会话 ID,就调用 createSession() 生成一个新的会话 ID(通常是一个 UUID),并在响应头里带上。 浏览器收到响应 会把这个 JSESSIONID 写入本地 Cookie 存储(因为你配置了 max-age=2592000,即 30 天,它会落盘保存,浏览器关了再开也不会丢失)。 后续请求 浏览器会自动在请求头里带上 Cookie: JSESSIONID=<新ID>,服务端就能根据这个 ID 从 Redis 里拿到对应的 Session 数据,恢复用户状态。 1)服务器内建一张 Map(或 Redis 等持久化存储),大致结构: { "abc123" -> HttpSession 实例 } 2)HttpSession 实例 自身又是一个 KV 容器,结构类似: HttpSession ├─ id = "abc123" ├─ creationTime = ... ├─ lastAccessedTime = ... └─ attributes └─ "USER_LOGIN_STATE" -> user 实体对象 3)请求流程 ┌───────────────┐ (带 Cookie JSESSIONID=abc123) │ Browser │ ───────►│ Tomcat │ └───────────────┘ └──────────┘ │ │ 用 abc123 做 key ▼ {abc123 → HttpSession} ← 找到 │ ▼ 取 attributes["USER_LOGIN_STATE"] → 得到 user request.getSession().setAttribute(UserConstant.USER_LOGIN_STATE, user); 后端代码的request.getSession()能自动获取当前请求所对应的HttpSession 实例!!!再往里存user信息。 3)退出登录 // 移除登录态 request.getSession().removeAttribute(UserConstant.USER_LOGIN_STATE); 此时,后端当前sessionId所对应的HttpSession 实例实例中的键"UserConstant.USER_LOGIN_STATE",它的值清零了(相当于用户信息删除了)。 Session 底层是基于Cookie实现的会话跟踪,因此Cookie的缺点他也有。 优点:Session是存储在服务端的,安全。会话数据存在客户端有篡改的风险。 缺点: 在分布式服务器集群环境下,Session 无法自动共享(可以共用redis解决) 如果客户端禁用 Cookie,Session 会失效。 需要在服务器端存储会话信息,可能带来性能压力,尤其是在高并发环境下。 流程解析 首次请求时(无 JSESSIONID Cookie): request.getSession() 会 自动创建新 Session,生成一个随机 JSESSIONID(如 abc123)。 服务器通过响应头 Set-Cookie: JSESSIONID=abc123; Path=/; HttpOnly 将 JSESSIONID 发给浏览器。 用户数据 user 被保存在服务器端,键为 USER_LOGIN_STATE,与 JSESSIONID 绑定。 后续请求时: 浏览器自动携带 Cookie: JSESSIONID=abc123。 服务器用 JSESSIONID 找到对应的 HttpSession,再通过 getAttribute("USER_LOGIN_STATE") 取出用户数据。 令牌JWT(推荐) 优点: 支持PC端、移动端 解决集群环境下的认证问题 减轻服务器的存储压力(无需在服务器端存储) 缺点:需要自己实现(包括令牌的生成、令牌的传递、令牌的校验) 跨域问题 跨域问题指的是在浏览器中,一个网页试图去访问另一个域下的资源时,浏览器出于安全考虑,默认会阻止这种操作。这是浏览器的同源策略(Same-Origin Policy)导致的行为。 同源策略(Same-Origin Policy) 同源策略是浏览器的一种安全机制,它要求: 协议(如 http、https) 域名/IP(如 example.com) 端口(如 80 或 443) 这三者必须完全相同,才能被视为同源。 举例: http://192.168.150.200/login.html ----------> https://192.168.150.200/login [协议不同,跨域] http://192.168.150.200/login.html ----------> http://192.168.150.100/login [IP不同,跨域] http://192.168.150.200/login.html ----------> http://192.168.150.200:8080/login [端口不同,跨域] http://192.168.150.200/login.html ----------> http://192.168.150.200/login [不跨域] 解决跨域问题的方法: CORS(Cross-Origin Resource Sharing)是解决跨域问题的标准机制。它允许服务器在响应头中加上特定的 CORS 头部信息,明确表示允许哪些外域访问其资源。 服务器端配置:服务器返回带有 Access-Control-Allow-Origin 头部的响应,告诉浏览器允许哪些域访问资源。 Access-Control-Allow-Origin: *(表示允许所有域访问) Access-Control-Allow-Origin: http://site1.com(表示只允许 http://site1.com 访问) 全局统一配置 import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class WebCorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/api/**") // 匹配所有 /api/** 路径 .allowedOrigins("http://allowed-domain.com") // 允许的域名 .allowedMethods("GET","POST","PUT","DELETE","OPTIONS") .allowedHeaders("Content-Type","Authorization") .allowCredentials(true) // 是否允许携带 Cookie .maxAge(3600); // 预检请求缓存 1 小时 } } Nginx解决方案 统一域名入口: 前端和 API 均通过 Nginx 以相同的域名(例如 https://example.com)提供服务。前端发送 AJAX 请求时,目标也是该域名的地址,如 https://example.com/api,从而避免了跨域校验。 Nginx 作为中间代理: Nginx 将特定路径(例如 /api/)的请求转发到后端服务器。对浏览器来说,请求和响应均来自同一域名,代理过程对浏览器透明。 “黑匣子”处理: 浏览器只与 Nginx 交互,不关心 Nginx 内部如何转发请求。无论后端位置如何,浏览器都认为响应源自统一域名,从而解决跨域问题。 总结 普通的跨域请求依然会送达服务器,服务器并不主动拦截;它只是通过响应头声明哪些来源被允许访问,而真正的拦截与安全检查,则由浏览器根据同源策略来完成。 JWT令牌 特性 Session JWT(JSON Web Token) 存储方式 服务端存储会话数据(如内存、Redis) 客户端存储完整的令牌(通常在 Header 或 Cookie) 标识方式 客户端持有一个 Session ID 客户端持有一个自包含的 Token 状态管理 有状态(Stateful),服务器要维护会话 无状态(Stateless),服务器不存会话 生成和校验 引入依赖 <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency> 生成令牌与解析令牌: public class JwtUtils { private static String signKey = "zy123"; private static Long expire = 43200000L; //单位毫秒 12小时 /** * 生成JWT令牌 * @param claims JWT第二部分负载 payload 中存储的内容 * @return */ public static String generateJwt(Map<String, Object> claims){ String jwt = Jwts.builder() .addClaims(claims) .signWith(SignatureAlgorithm.HS256, signKey) .setExpiration(new Date(System.currentTimeMillis() + expire)) .compact(); return jwt; } /** * 解析JWT令牌 * @param jwt JWT令牌 * @return JWT第二部分负载 payload 中存储的内容 */ public static Claims parseJWT(String jwt){ Claims claims = Jwts.parser() .setSigningKey(signKey) .parseClaimsJws(jwt) .getBody(); return claims; } } 令牌可以存储当前登录用户的信息:id、username等等,传入claims Object 类型能够容纳字符串、数字等各种对象。 Map<String, Object> claims = new HashMap<>(); claims.put("id", emp.getId()); // 假设 emp.getId() 返回一个数字(如 Long 类型) claims.put("name", e.getName()); // 假设 e.getName() 返回一个字符串 claims.put("username", e.getUsername()); // 假设 e.getUsername() 返回一个字符串 String jwt = JwtUtils.generateJwt(claims); 解析令牌: @Autowired private HttpServletRequest request; String jwt = request.getHeader("token"); Claims claims = JwtUtils.parseJWT(jwt); // 解析 JWT 令牌 // 获取存储的 id, name, username Long id = (Long) claims.get("id"); // 如果 "id" 是 Long 类型 String name = (String) claims.get("name"); String username = (String) claims.get("username"); JWT 登录认证流程 用户登录 用户发起登录请求,校验密码、登录成功后,生成 JWT 令牌,并将其返回给前端。 前端存储令牌 前端接收到 JWT 令牌,存储在浏览器中(通常存储在 LocalStorage 或 Cookie 中)。 // 登录成功后,存储 JWT 令牌到 LocalStorage const token = response.data.token; // 从响应中获取令牌 localStorage.setItem('token', token); // 存储到 LocalStorage // 在后续请求中获取令牌并附加到请求头 const storedToken = localStorage.getItem('token'); fetch("https://your-api.com/protected-endpoint", { method: "GET", headers: { "token": storedToken // 添加 token 到请求头 } }) .then(response => response.json()) .then(data => console.log(data)) .catch(error => console.log('Error:', error)); 请求带上令牌 后续的每次请求,前端将 JWT 令牌携带上。 服务端校验令牌 服务端接收到请求后,拦截请求并检查是否携带令牌。若没有令牌,拒绝访问;若令牌存在,校验令牌的有效性(包括有效期),若有效则放行,进行请求处理。 注意,使用APIFOX测试时,需要在headers中添加 {token:"jwt令牌..."}否则会无法通过拦截器。 拦截器(Interceptor) 在拦截器当中,我们通常也是做一些通用性的操作,比如:我们可以通过拦截器来拦截前端发起的请求,将登录校验的逻辑全部编写在拦截器当中。在校验的过程当中,如发现用户登录了(携带JWT令牌且是合法令牌),就可以直接放行,去访问spring当中的资源。如果校验时发现并没有登录或是非法令牌,就可以直接给前端响应未登录的错误信息。 快速入门 定义拦截器,实现HandlerInterceptor接口,并重写其所有方法 //自定义拦截器 @Component public class JwtTokenUserInterceptor implements HandlerInterceptor { //目标资源方法执行前执行。 返回true:放行 返回false:不放行 @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { System.out.println("preHandle .... "); return true; //true表示放行 } //目标资源方法执行后执行 @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { System.out.println("postHandle ... "); } //视图渲染完毕后执行,最后执行 @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { System.out.println("afterCompletion .... "); } } 注意: preHandle方法:目标资源方法执行前执行。 返回true:放行 返回false:不放行 postHandle方法:目标资源方法执行后执行 afterCompletion方法:视图渲染完毕后执行,最后执行 注册配置拦截器,实现WebMvcConfigurer接口,并重写addInterceptors方法 @Configuration public class WebConfig implements WebMvcConfigurer { //自定义的拦截器对象 @Autowired private JwtTokenUserInterceptor jwtTokenUserInterceptor; @Override protected void addInterceptors(InterceptorRegistry registry) { log.info("开始注册自定义拦截器..."); registry.addInterceptor(jwtTokenUserInterceptor) .addPathPatterns("/user/**") .excludePathPatterns("/user/user/login") .excludePathPatterns("/user/shop/status"); } } WebMvcConfigurer接口: 拦截器配置 通过实现 addInterceptors 方法,可以添加自定义的拦截器,从而在请求进入处理之前或之后执行一些逻辑操作,如权限校验、日志记录等。 静态资源映射 通过 addResourceHandlers 方法,可以自定义静态资源(如 HTML、CSS、JavaScript)的映射路径,这对于使用前后端分离或者集成第三方文档工具(如 Swagger/Knife4j)非常有用。 消息转换器扩展 通过 extendMessageConverters 方法,可以在默认配置的基础上,追加自定义的 HTTP 消息转换器,如将 Java 对象转换为 JSON 格式。 跨域配置 使用 addCorsMappings 方法,可以灵活配置跨域资源共享(CORS)策略,方便前后端跨域请求。 拦截路径 addPathPatterns指定拦截路径; 调用excludePathPatterns("不拦截的路径")方法,指定哪些资源不需要拦截。 拦截路径 含义 举例 /* 一级路径 能匹配/depts,/emps,/login,不能匹配 /depts/1 /** 任意级路径 能匹配/depts,/depts/1,/depts/1/2 /depts/* /depts下的一级路径 能匹配/depts/1,不能匹配/depts/1/2,/depts /depts/** /depts下的任意级路径 能匹配/depts,/depts/1,/depts/1/2,不能匹配/emps/1 登录校验 主要在preHandle中写逻辑 public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //判断当前拦截到的是Controller的方法还是其他资源 if (!(handler instanceof HandlerMethod)) { //当前拦截到的不是动态方法,直接放行 return true; } //1、从请求头中获取令牌 String token = request.getHeader(jwtProperties.getUserTokenName()); //2、校验令牌 try { log.info("jwt校验:{}", token); Claims claims = JwtUtil.parseJWT(jwtProperties.getUserSecretKey(), token); Long userId = Long.valueOf(claims.get(JwtClaimsConstant.USER_ID).toString()); log.info("当前用户id:", userId); BaseContext.setCurrentId(userId); //3、通过,放行 return true; } catch (Exception ex) { //4、不通过,响应401状态码 response.setStatus(401); return false; } } 全局异常处理 **当前问题:**如果程序因不知名原因报错,响应回来的数据是一个JSON格式的数据,但这种JSON格式的数据不符合开发规范当中所提到的统一响应结果Result,导致前端不能解析出响应的JSON数据。 当我们没有做任何的异常处理时,我们三层架构处理异常的方案: Mapper接口在操作数据库的时候出错了,此时异常会往上抛(谁调用Mapper就抛给谁),会抛给service。 service 中也存在异常了,会抛给controller。 而在controller当中,我们也没有做任何的异常处理,所以最终异常会再往上抛。最终抛给框架之后,框架就会返回一个JSON格式的数据,里面封装的就是错误的信息,但是框架返回的JSON格式的数据并不符合我们的开发规范。 如何解决: 方案一:在所有Controller的所有方法中进行try…catch处理 缺点:代码臃肿(不推荐) 方案二:全局异常处理器 好处:简单、优雅(推荐) 全局异常处理 定义全局异常处理器非常简单,就是定义一个类,在类上加上一个注解**@RestControllerAdvice**,加上这个注解就代表我们定义了一个全局异常处理器。 在全局异常处理器当中,需要定义一个方法来捕获异常,在这个方法上需要加上注解**@ExceptionHandler**。通过@ExceptionHandler注解当中的value属性来指定我们要捕获的是哪一类型的异常。 @RestControllerAdvice public class GlobalExceptionHandler { //处理 RuntimeException 异常 @ExceptionHandler(RuntimeException.class) public Result handleRuntimeException(RuntimeException e) { e.printStackTrace(); return Result.error("系统错误,请稍后再试"); } // 处理 NullPointerException 异常 @ExceptionHandler(NullPointerException.class) public Result handleNullPointerException(NullPointerException e) { e.printStackTrace(); return Result.error("空指针异常,请检查代码逻辑"); } //处理异常 @ExceptionHandler(Exception.class) //指定能够处理的异常类型,Exception.class捕获所有异常 public Result ex(Exception e){ e.printStackTrace();//打印堆栈中的异常信息 //捕获到异常之后,响应一个标准的Result return Result.error("对不起,操作失败,请联系管理员"); } } 模拟NullPointerException String str = null; // 调用 null 对象的方法会抛出 NullPointerException System.out.println(str.length()); // 这里会抛出 NullPointerException 模拟RuntimeException int res=10/0; 事务 场景与问题: @Slf4j @Service public class DeptServiceImpl implements DeptService { @Autowired private DeptMapper deptMapper; @Autowired private EmpMapper empMapper; //根据部门id,删除部门信息及部门下的所有员工 @Override public void delete(Integer id){ //根据部门id删除部门信息 deptMapper.deleteById(id); //模拟:异常发生 int i = 1/0; //删除部门下的所有员工信息 empMapper.deleteByDeptId(id); } } 问题:出现异常后,部门已被删除,但员工记录仍然存在,造成数据不一致。 原因:整组操作没有被事务包裹,无法做到“要么全部成功,要么全部失败”。 @Transactional 注解 位置 作用 方法级 仅当前方法受事务管理 类级 类中所有方法受事务管理 接口级 接口下 所有实现类 的全部方法受事务管理 在实际开发中,推荐只在 Service 层的方法或类上标注,保持粒度清晰。 常用属性 属性 说明 默认值 rollbackFor 指定哪些异常触发回滚 仅 RuntimeException propagation 指定事务传播行为 Propagation.REQUIRED ① 回滚规则(rollbackFor) @Transactional(rollbackFor = Exception.class) // 捕获所有异常并回滚 public void delete(Integer id) { ... } 如果只写 @Transactional,则 仅 运行时异常(RuntimeException)会触发回滚。 如要让 检查时异常(Exception)也能回滚,就需显式指定 rollbackFor。 ② 事务传播行为(propagation) 传播行为 父事务已存在时 父事务不存在时 典型用途 / 说明 REQUIRED (默认) 加入父事务→ 共提交 / 回滚 创建新事务 日常业务写操作,保持一致性 REQUIRES_NEW 挂起父事务→ 自己新建事务 自己新建事务 写日志、发送 MQ 等:外层失败也要单独成功 SUPPORTS 加入父事务 非事务方式执行 只读查询:有事务跟随一致性,没有就轻量查询 NOT_SUPPORTED 挂起父事务→ 非事务方式执行 非事务方式执行 大批量/耗时操作,避免长事务锁表 MANDATORY 加入父事务 立即抛异常 防御性编程:强制要求调用方已开启事务 NEVER 立即抛异常 非事务方式执行 禁止在事务里跑的代码(如特殊 DDL) NESTED 同一物理事务,打 SAVEPOINT→ 子回滚只回到保存点 创建新事务(与 REQUIRED 效果相同) 分段回滚;需 DB / JDBC 支持保存点 需要“互不影响”时用 REQUIRES_NEW——强制新建事务: @Transactional // 外层保存订单 public void saveOrder(Order order){ orderMapper.insert(order); // 总是单独提交日志 logService.saveLog(...); // 后面出现异常 if(order.getAmount() < 0){ throw new IllegalArgumentException("非法金额"); } } //操作日志、审计表、MQ 消息等,不能因为业务失败而丢记录。 @Transactional(propagation = Propagation.REQUIRES_NEW) public void saveLog(Log log) { ... } 调试:Spring事务日志开关 在 application.yml 中添加: logging: level: org.springframework.jdbc.support.JdbcTransactionManager: debug 效果:控制台会打印事务生命周期日志(开启、提交、回滚等),方便排查。 总结 当 Service 层发生异常 时,Spring 会按照以下顺序处理: 事务的回滚:如果 Service 层抛出了一个异常(如 RuntimeException),并且这个方法是 @Transactional 注解标注的,Spring 会在方法抛出异常时 回滚事务。Spring 事务管理器会自动触发回滚操作。 异常传播到 Controller 层:如果异常在 Service 层处理后未被捕获,它会传播到 Controller 层(即调用 Service 方法的地方)。 全局异常处理器:当异常传播到 Controller 层时,全局异常处理器(@RestControllerAdvice 或 @ControllerAdvice)会捕获并处理该异常,返回给前端一个标准的错误响应。 AOP AOP(Aspect-Oriented Programming,面向切面编程)是一种编程思想,旨在将横切关注点(如日志、性能监控等)从核心业务逻辑中分离出来。简单来说,AOP 是通过对特定方法的增强(如统计方法执行耗时)来实现代码复用和关注点分离。 快速入门 实现业务方法执行耗时统计的步骤 定义模板方法:将记录方法执行耗时的公共逻辑提取到模板方法中。 记录开始时间:在方法执行前记录开始时间。 执行原始业务方法:中间部分执行实际的业务方法。 记录结束时间:在方法执行后记录结束时间,计算并输出执行时间。 通过 AOP,我们可以在不修改原有业务代码的情况下,完成对方法执行耗时的统计。 实现步骤: 导入依赖:在pom.xml中导入AOP的依赖 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> 编写AOP程序:针对于特定方法根据业务需要进行编程 @Component @Aspect //当前类为切面类 @Slf4j public class TimeAspect { ////第一个星号表示任意返回值,第二个星号表示类/接口,第三个星号表示所有方法。 @Around("execution(* edu.whut.zy123.service.*.*(..))") public Object recordTime(ProceedingJoinPoint pjp) throws Throwable { //记录方法执行开始时间 long begin = System.currentTimeMillis(); //执行原始方法 Object result = pjp.proceed(); //记录方法执行结束时间 long end = System.currentTimeMillis(); //计算方法执行耗时,pjp.getSignature()获得函数名 log.info(pjp.getSignature()+"执行耗时: {}毫秒",end-begin); return result; } } 我们通过AOP入门程序完成了业务方法执行耗时的统计,那其实AOP的功能远不止于此,常见的应用场景如下: 记录系统的操作日志 权限控制 事务管理:我们前面所讲解的Spring事务管理,底层其实也是通过AOP来实现的,只要添加@Transactional注解之后,AOP程序自动会在原始方法运行前先来开启事务,在原始方法运行完毕之后提交或回滚事务 核心概念 1. 连接点:JoinPoint,可以被AOP控制的方法,代表方法的执行位置 2. 通知:Advice,指对目标方法的“增强”操作 (体现为额外的代码) 3. 切入点:PointCut,是一个表达式,匹配连接点的条件,它指定了 在目标方法的哪些位置插入通知,比如在哪些方法调用之前、之后、或者哪些方法抛出异常时进行增强。 4. 切面:Aspect,通知与切入点的结合 5.目标对象:Target,被 AOP 代理的对象,通知会作用到目标对象的对应方法上。 示例: @Slf4j @Component @Aspect public class MyAspect { @Before("execution(* edu.whut.zy123.service.MyService.doSomething(..))") public void beforeMethod(JoinPoint joinPoint) { // 连接点:目标方法执行位置 System.out.println("Before method: " + joinPoint.getSignature().getName()); } } joinPoint 代表的是 doSomething() 方法执行的连接点。 beforeMethod() 方法就是一个前置通知 "execution(* com.example.service.MyService.doSomething(..))"是切入点 MyAspect是切面。 com.example.service.MyService 类的实例是目标对象 通知类型 @Around:环绕通知。此通知会在目标方法前后都执行。 @Before:前置通知。此通知在目标方法执行之前执行。 @After :后置通知。此通知在目标方法执行后执行,无论方法是否抛出异常。 @AfterReturning : 返回后通知。此通知在目标方法正常返回后执行,发生异常时不会执行。 @AfterThrowing : 异常后通知。此通知在目标方法抛出异常后执行。 在使用通知时的注意事项: @Around 通知必须调用 ProceedingJoinPoint.proceed() 才能执行目标方法,其他通知不需要。 @Around 通知的返回值必须是 Object 类型,用于接收原始方法的返回值。 只有@Around需要在通知中主动执行方法,其他通知只能获取目标方法的参数等。 通知执行顺序 默认情况下,不同切面类的通知执行顺序由类名的字母顺序决定。 可以通过 @Order 注解指定切面类的执行顺序,数字越小,优先级越高。 例如:@Order(1) 表示该切面类的通知优先执行。 @Aspect @Order(1) // 优先级1 @Component public class AspectOne { @Before("execution(* edu.whut.zy123.service.MyService.*(..))") public void beforeMethod() { System.out.println("AspectOne: Before method"); } } @Aspect @Order(2) // 优先级2 @Component public class AspectTwo { @Before("execution(* edu.whut.zy123.service.MyService.*(..))") public void beforeMethod() { System.out.println("AspectTwo: Before method"); } } 如果调用 MyService 中的某个方法,AspectOne切面类中的通知会先执行。 结论:目标方法前的通知方法,Order小的或者类名的字母顺序在前的先执行。 目标方法后的通知方法,Order小的或者类名的字母顺序在前的后执行。 相对于显式设置(Order)的通知,默认通知的优先级最低。 切入点表达式 作用:主要用来决定项目中的哪些方法需要加入通知 常见形式: execution(……):根据方法的签名来匹配 @annotation(……) :根据注解匹配 execution execution主要根据方法的返回值、包名、类名、方法名、方法参数等信息来匹配,语法为: execution(访问修饰符? 返回值 包名.类名.?方法名(方法参数) throws 异常?) 其中带?的表示可以省略的部分 访问修饰符:可省略(比如: public、protected) 包名.类名.: 可省略,但不建议 throws 异常:可省略(注意是方法上声明抛出的异常,不是实际抛出的异常) 示例: //如果希望匹配 public void delete(Integer id) @Before("execution(void edu.whut.zy123.service.impl.DeptServiceImpl.delete(java.lang.Integer))") //如果希望匹配 public void delete(int id) @Before("execution(void edu.whut.zy123.service.impl.DeptServiceImpl.delete(int))") 在 Pointcut 表达式中,为了确保匹配准确,通常建议对非基本数据类型使用全限定名。这意味着,对于像 Integer 这样的类,最好写成 java.lang.Integer 可以使用通配符描述切入点 * :单个独立的任意符号,可以通配任意返回值、包名、类名、方法名、任意类型的一个参数,也可以通配包、类、方法名的一部分 execution(* edu.*.service.*.update*(*)) 这里update后面的'星'即通配方法名的一部分,()中的'星'表示有且仅有一个任意参数 可以匹配: package edu.zju.service; public class UserService { public void updateUser(String username) { // 方法实现 } } .. :多个连续的任意符号,可以通配任意层级的包,或任意类型、任意个数的参数 execution(* com.example.service.UserService.*(..)) 公共表示@Pointcut 使用 @Pointcut 注解可以将切点表达式提取到一个独立的方法中,提高代码复用性和可维护性。 @Aspect @Component public class LoggingAspect { // 定义一个切点,匹配com.example.service包下 UserService 类的所有方法 @Pointcut("execution(public * com.example.service.UserService.*(..))") public void userServiceMethods() { // 该方法仅用来作为切点标识,无需实现任何内容 } // 在目标方法执行前执行通知,引用上面的切点 @Before("userServiceMethods()") public void beforeUserServiceMethods() { System.out.println("【日志】即将执行 UserService 中的方法"); } } annotation 那么如果我们要匹配多个无规则的方法,比如:list()和 delete()这两个方法。我们可以借助于另一种切入点表达式annotation来描述这一类的切入点,从而来简化切入点表达式的书写。 实现步骤: 新建anno包,在这个包下编写自定义注解 import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; // 定义注解 @Retention(RetentionPolicy.RUNTIME) // 定义注解的生命周期 @Target(ElementType.METHOD) // 定义注解可以应用的Java元素类型 public @interface MyLog { // 定义注解的元素(属性) String description() default "This is a default description"; int value() default 0; } 在业务类要做为连接点的方法上添加自定义注解 @MyLog //自定义注解(表示:当前方法属于目标方法) public void delete(Integer id) { //1. 删除部门 deptMapper.delete(id); } AOP切面类上使用类似如下的切面表达式: @Before("@annotation(edu.whut.anno.MyLog)") 连接点JoinPoint 执行: ProceedingJoinPoint和 JoinPoint 都是调用 proceed() 就会执行被代理的方法 Object result = joinPoint.proceed(); 获取调用方法时传递的参数,即使只有一个参数,也以数组形式返回: Object[] args = joinPoint.getArgs(); getSignature(): 返回一个Signature类型的对象,这个对象包含了被拦截点的签名信息。在方法调用的上下文中,这包括了方法的名称、声明类型等信息。 方法名称:可以通过调用getName()方法获得。 声明类型:方法所在的类或接口的完全限定名,可以通过getDeclaringTypeName()方法获取。 返回类型(对于方法签名):可以通过将Signature对象转换为更具体的MethodSignature类型,并调用getReturnType()方法获取。 WEB开发总体图
后端学习
zy123
3月21日
0
18
0
上一页
1
2
3
4
下一页