Java:113-Spring Data JPA详解

Spring Data JPA详解

Spring Data Jpa 是应用于Dao层的⼀个框架,简化数据库开发的,作用和Mybatis框架⼀样,但是在使用方式和底层机制是有所不同的,最明显的⼀个特点,Spring Data Jpa 开发Dao的时候,很多场景我们 连sql语句都不需要开发,且由Spring出品
Spring Data JPA 概述:
什么是 Spring Data JPA:
Spring Data JPA 是 Spring 基于JPA 规范的基础上封装的⼀套 JPA 应用框架,可使开发者用极简的代码即可实现对数据库的访问和操作,它提供了包括增删改查等在内的常用功能,学习并使用 Spring Data JPA 可以极大提高开发效率
说明:Spring Data JPA 极大简化了数据访问层代码
如何简化呢,使用了Spring Data JPA,我们Dao层中只需要写接口,不需要写实现类,就自动具有 了增删改查,分页查询等方法
使用Spring Data JPA 很多场景下不需要我们自己写sql语句
什么是JPA 规范:Java Persistence API(JPA) 是一套用于管理和持久化 Java 对象与关系数据库之间数据的标准规范,JPA 主要用来将 Java 对象与数据库中的表记录相互映射,通过对象化的方式进行数据库操作,简化了数据库的访问
注意了,对应的与ORM的区别是:ORM 是一种技术,它通过将数据库表映射到对象来实现对象和关系数据之间的转换,ORM 的目标是使开发者能够以面向对象的方式与数据库交互,而不必编写大量的 SQL 语句,典型是mybatis,通常使用一些xml来对应类,而JPA规范通常是使用一些注解来进行操作,所以如果mybatis本身也可以完全使用注解来完成,那么也可以称mybatis是一种JPA规范操作的框架(虽然现实并不是)
简单来说:
JPA是一种标准规范,定义了如何通过注解和实体类进行对象关系映射(ORM),JPA 注重自动化,通过注解和标准 API(如 EntityManager)来管理实体的生命周期、关系和持久化操作
ORM侧重于映射,如MyBatis 提供了一种将 SQL 查询直接映射到 Java 方法的方式,并且允许开发者手动编写 SQL 查询,这使得它非常灵活,但与 JPA 的自动化和规范化有很大的不同,但是呢,JPA最终也是操作映射的,所以可以称JPA也是一种ORM的规范,即JPA是一个实现了ORM概念的Java规范(前提是有使用了这个规范的框架,否则只是规范)
Spring Data 家族:

在这里插入图片描述

Spring Data JPA,JPA规范和Hibernate之间的 关系
Spring Data JPA 是 Spring 提供的⼀个封装了JPA 操作的框架,而JPA 仅仅是规范,单独使用规范无法 具体做什么,那么Spring Data JPA 、 JPA规范 以及 Hibernate (是JPA 规范的⼀种实现,了解这个即可)之间的关系是什么:

在这里插入图片描述

即:JPA 是⼀套规范,内部是由接口和抽象类组成的,Hiberanate 是⼀套成熟的 ORM 框架,而且 Hiberanate 实现了 JPA 规范,所以可以称 Hiberanate 为 JPA 的⼀种实现方式,我们使用 JPA 的 API 编程,意味着站在更高的角度去看待问题(面向接口编程), Spring Data JPA 是 Spring 提供的⼀套对 JPA 操作更加⾼级的封装,是在 JPA 规范下的专门用来进行数 据持久化的解决方案
也就是说,Spring Data JPA封装后,需要使用该jpa规范的框架,可以直接使用封装好的(也就是Spring Data JPA),而不用自行实现了(或者不用使用自身的,因为Spring Data JPA在原来的jpa基础上有所增强)
也要注意:虽然JPA定义了接口和规范,但它并没有提供实际的实现代码,因此,需要一个具体的实现来执行这些操作,Hibernate是JPA规范的一个常见实现,提供了实际的持久化逻辑,所以当Hibernate操作时,通常建议与该jpa整合,使得Hibernate也使用这个增强的规范,使得我们在这个规范下编写代码,从而不用自身的不增强的jpa了
说了这么多,其实就一句话:有jpa规范的框架,自身jpa还不够,我需要增强的jpa(Spring Data JPA)
Spring Data JPA 应用:
需求:使用 Spring Data JPA 完成对 tb_resume 表(简历表)的Dao 层操作(增删改查,排序, 分页等)
创建数据库:
CREATE DATABASE wd CHARACTER SET utf8;
USE wd;
CREATE TABLE tb_resume(
 id BIGINT(20) NOT NULL AUTO_INCREMENT,
 address VARCHAR(255) DEFAULT NULL,
 NAME VARCHAR(255) DEFAULT NULL,
 phone VARCHAR(255) DEFAULT NULL,
 PRIMARY KEY (`id`)
);
INSERT INTO tb_resume VALUES (1, '北京', '张三', '131000000');
INSERT INTO tb_resume VALUES (2, '上海', '李四', '151000000');
INSERT INTO tb_resume VALUES (3, '广州', '王五', '153000000');
开发步骤:
我们这里操作的是整合Hibernate的,而不是单纯的Hibernate,所以呢,如果需要学习单纯的Hibernate,那么可以百度(使用整合的其实就够啦,这个整合其实不只是jpa规范继续增强,也与Spring整合了,类似mybatis整合spring,只不过这里多了一个jpa规范而已,或者说多几个注解)
首先创建项目,然后我们引入依赖:
<?xml version="1.0" encoding="UTF-8"?>
<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>org.example</groupId>
    <artifactId>jpa</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
    </properties>
    <dependencies>
        <!--单元测试jar-->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>

        <!--spring-data-jpa 需要引⼊的jar,start-->
        <dependency>
             <!--存在xml配置中的命名空间:xmlns:jpa="http://www.springframework.org/schema/data/jpa
            所以通常需要

还有对应的注解,如dao接口中的ResumeDao的@Query,在后面会知道的
            -->
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-jpa</artifactId>
            <version>2.1.8.RELEASE</version>
        </dependency>
        <dependency>
            <!--这个依赖,通常情况下不是必须的,也就是说,可以去掉
一般解释为:如果项目中有使用 JSP,并且页面中使用了 EL 表达式,那么 javax.el 依赖是必须的
-->
            <groupId>org.glassfish.web</groupId>
            <artifactId>javax.el</artifactId>
            <version>2.2.6</version>
        </dependency>

        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.8.13</version>
        </dependency>
        <dependency>
            <!--Spring容器,即IOC容器的使用需要这个,比如ApplicationContext-->
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.1.12.RELEASE</version>
        </dependency>
        <dependency>
            <!--里面存在对于的jdbc的依赖和tx依赖,相当于是操作spring-jdbc依赖,但是他依赖自身也存在代码的
            他一般用来操作:
            -->
            <groupId>org.springframework</groupId>
            <artifactId>spring-orm</artifactId>
            <version>5.1.12.RELEASE</version>
        </dependency>

        <!--hibernate对jpa的实现jar-->
        <dependency>
            <!--对应的依赖,当需要指定一个框架时,如:
<property name="persistenceProvider">
            <bean class="org.hibernate.jpa.HibernatePersistenceProvider"></bean>
        </property>

那么就是需要的
还存在对应的注解:如后面实体类Resume的@Entity,在后面会知道的
-->
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-entitymanager</artifactId>
            <version>5.4.0.Final</version>
        </dependency>
        <dependency>
            <!--用于一些其他的操作,比如:
注解验证:在实体类中使用注解,如 @NotNull,@Size,@Min,@Max 等,以便在数据保存到数据库之前进行验证
当不用时,那么这个依赖可以去掉
-->
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-validator</artifactId>
            <version>5.4.0.Final</version>
        </dependency>


        <!--mysql 数据库驱动jar-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.46</version>
        </dependency>
        <!--druid连接池-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.21</version>
        </dependency>
        <!--spring-test,测试的-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>5.1.12.RELEASE</version>
        </dependency>
    </dependencies>
</project>
创建jdbc.properties文件:
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/wd?characterEncoding=utf8&useSSL=false
jdbc.username=root
jdbc.password=123456
创建com.pojo包,然后创建Resume类:
package com.pojo;

import javax.persistence.*;

/**
 * 简历实体类(在类中要使⽤注解建⽴实体类和数据表之间的映射关系以及属性和字段的映射关系)
 * 实体类和数据表映射关系
 *
 * @Entity
 * @Table 实体类属性和表字段的映射关系
 * @Id 标识主键
 * @GeneratedValue 标识主键的⽣成策略
 * @Column 建⽴属性和字段映射
 */
@Entity
@Table(name = "tb_resume")
public class Resume {
    @Id
/**
 * ⽣成策略经常使⽤的两种:
 * GenerationType.IDENTITY:依赖数据库中主键⾃增功能 Mysql
 * GenerationType.SEQUENCE:依靠序列来产⽣主键 Oracle
 */
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Long id;
    @Column(name = "name")
    private String name;
    @Column(name = "address")
    private String address;
    @Column(name = "phone") //里面对应与数据库中的字段
    private String phone;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }

    public String getPhone() {
        return phone;
    }

    public void setPhone(String phone) {
        this.phone = phone;
    }

    @Override
    public String toString() {
        return "Resume{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", address='" + address + '\'' +
                ", phone='" + phone + '\'' +
                '}';
    }
}
创建com.dao包,然后创建ResumeDao接口:
package com.dao;

import com.pojo.Resume;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;

/**
 * ⼀个符合SpringDataJpa要求的Dao层接⼝是需要继承JpaRepository和
 * JpaSpecificationExecutor
 * JpaRepository<操作的实体类类型,主键类型>
 * 封装了基本的CRUD操作
 * JpaSpecificationExecutor<操作的实体类类型>
 * 封装了复杂的查询(分⻚、排序等)
 */
public interface ResumeDao extends JpaRepository<Resume, Long>,
        JpaSpecificationExecutor<Resume> {


}
配置 Spring 的配置文件,spring.xml:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:jpa="http://www.springframework.org/schema/data/jpa"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/tx
        http://www.springframework.org/schema/tx/spring-tx.xsd
        http://www.springframework.org/schema/data/jpa
        https://www.springframework.org/schema/data/jpa/spring-jpa.xsd">

    <!--配置spring包扫描-->
    <context:component-scan base-package="com"/>

    <!--对Spring和SpringDataJPA进⾏配置-->
    <!--创建数据库连接池druid-->
    <!--引⼊外部资源⽂件-->
    <context:property-placeholder location="classpath:jdbc.properties"/>
    <!--第三⽅jar中的bean定义在xml中-->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="${jdbc.driver}"/>
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>

    <!--配置⼀个JPA中⾮常重要的对象,entityManagerFactory,entityManager类似于mybatis中的SqlSession
    entityManagerFactory类似于Mybatis中的SqlSessionFactory

    当然,这里的配置有点像对应的mybatis与spring整合的处理,即:
      <bean id="sessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">

    -->
    <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <!--配置⼀些细节-->
        <!--配置数据源-->
        <property name="dataSource" ref="dataSource"/>
        <!--配置包扫描(pojo实体类所在的包,名称处理)-->
        <property name="packagesToScan" value="com.pojo"/>
        <!--指定jpa的具体实现,也就是hibernate,如果你不显式地指定persistenceProvider,Spring会尝试根据类路径上的依赖自动检测一个合适的JPA提供者(如果你已经在项目中引入了Hibernate的相关依赖,Spring会自动选择Hibernate作为JPA提供者),如果没有,那么可能会出现异常
这里是需要一个实现的,前面说明了,可以搜索"需要一个具体的实现来执行这些操作"

注意了:hibernate自身就有jpa规范,所以可以单独的使用hibernate,而不用进行整合,只不过Spring Data JPA是对JPA的一层抽象和增强,提供了一些方便的功能和工具,所以这里整合一下
换言之,这里的配置相当于替换了hibernate自身的jpa规范,也或者是对应的spring data jpa使用hibernate的实现
        -->
        <property name="persistenceProvider">
            <bean class="org.hibernate.jpa.HibernatePersistenceProvider"></bean>
        </property>
        <!--jpa方言配置,不同的jpa实现对于类似于beginTransaction等细节实现起来是不⼀样的,所以传⼊JpaDialect具体的实现类,这里指定的是 HibernateJpaDialect,表示使用Hibernate的方言实现
问题:既然上面给了提供者,为什么还需要方言,难道不会自动给出吗,答:通常会自动的处理,只是我们建议手动的指定,防止不会出现错误(如不小心设置了其他方言)
-->
        <property name="jpaDialect">
            <bean class="org.springframework.orm.jpa.vendor.HibernateJpaDialect"></bean>
        </property>
        <!--配置具体provider,hibearnte框架的执⾏细节-->
        <property name="jpaVendorAdapter">
            <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
                <!--定义hibernate框架的⼀些细节-->
                <!--配置数据表是否⾃动创建,因为我们会建⽴pojo和数据表之间的映射关系
                程序启动时,如果数据表还没有创建,是否要程序给创建⼀下
                -->
                <property name="generateDdl" value="false"/>
                <!--
                指定数据库的类型
                hibernate本身是个dao层框架,可以⽀持多种数据库类型的,这⾥就指定本次使⽤的什么数据库
                -->
                <property name="database" value="MYSQL"/>
                <!--
                配置数据库的方言
                hibernate可以帮助我们拼装sql语句,但是不同的数据库sql
               语法是不同的,所以需要我们注⼊具体的数据库⽅⾔,这个可以不写,因为上面指定了数据库类型,会自动考虑对应数据库的方言
               当然,这里可以进行覆盖,还有其实,他们两个可以独立的写的
但是我们还是建议手动的指定,防止不会出现错误(如不小心设置了其他方言)
                -->
                <property name="databasePlatform" value="org.hibernate.dialect.MySQLDialect"/>
                <!--是否显示sql
                操作数据库时,是否打印sql
删除了,那么对应操作代码时,可能没有sql语句看了哦
                -->
                <property name="showSql" value="true"/>
            </bean>
        </property>
    </bean>
    <!--引⽤上⾯创建的entityManagerFactory
    <jpa:repositories> 配置jpa的dao层细节
    base-package:指定dao层接⼝所在包
    entity-manager-factory-ref指定一个,如果只存在一个可以忽略,存在多个,自动选择一个(一般是先配置的)
    transaction-manager-ref指定一个,如果存在多个,通常必须指定,否则可能出现异常
    -->
    <jpa:repositories base-package="com.dao" entity-manager-factory-ref="entityManagerFactory"
                      transaction-manager-ref="transactionManager"/>
    <!--事务管理器配置
    jdbcTemplate/mybatis 使⽤的是DataSourceTransactionManager
    jpa规范:JpaTransactionManager
    -->
    <bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
        <property name="entityManagerFactory" ref="entityManagerFactory"/>
    </bean>
    <!--声明式事务配置,通常需要操作@Transactional注解时需要,相当于扫描对应的注解使得起作用-->
    <tx:annotation-driven/>
</beans>

<!--方言可以是语法(mysql),也可以是内部操作方式(jpa方言配置)-->
至此,对应的spring扫描,以及数据源配置,以及jpa工厂配置(整合了hibernate)
我们在测试资源文件夹下创建com.test包,然后创建Test1类:
package com.test;

import com.dao.ResumeDao;
import com.pojo.Resume;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import java.util.Optional;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:spring.xml"})
public class Test1 {

    // 要测试IOC哪个对象注⼊即可
    @Autowired
    private ResumeDao resumeDao;

    /**
     * dao层接⼝调⽤,分成两块:
     * 1、基础的增删改查
     * 2、专⻔针对查询的详细分析使⽤
     */

    @Test
    public void testFindById() {
        // 早期的版本 dao.findOne(id); 可能现在没有了
        Optional<Resume> optional = resumeDao.findById(1l);
        Resume resume = optional.get();
        System.out.println(resume);
        /*
        打印的结果(也包括sql语句):
        Hibernate: 
        select 
        resume0_.id as id1_0_0_, 
        resume0_.address as address2_0_0_, 
        resume0_.name as name3_0_0_, 
        resume0_.phone as phone4_0_0_ 
        from tb_resume resume0_ where resume0_.id=?
        Resume{id=1, name='张三', address='北京', phone='131000000'}
        
        很明显:对应的接口由于有对应的继承的泛型,那么会找到对应的类,然后通过类的信息进行处理
        sql语句是:类名称0_.类变量 as 类变量递增数_0_0
        类别名是:类名称0_
        还有:@Table(name = "tb_resume")和@Column(name = "id")中id是作为sql中变量存在的,如果是iD,那么就是resume0_.iD,而id和iD在mysql中是没有影响的(表字段忽略大小写)
        但是呢,他们是直接的指定,在mp中,是存在不指定,使用类来操作的,我们可以测试,发现他没有像mp一样的驼峰大小写,也就是说nAme就是nAme,不指定也算的(当然了,这些小细节忽略即可,没有必要的)
        */
    }


}

直接进行访问,若出现了数据,代表操作成功,至此我们搭建好了环境的处理
我们继续学习:
继续在该测试类中补充如下:
@Test
    public void testFindOne(){
        Resume resume = new Resume();
        resume.setId(1l);
        Example<Resume> of = Example.of(resume);
        Optional<Resume> one = resumeDao.findOne(of); //操作条件
        System.out.println(one.get()); //get得到对应的Resume
        //当然了,对应get里面如果没有数据,会出现异常:
        /*
         public T get() {
        if (value == null) {
            throw new NoSuchElementException("No value present");
        }
        return value;
    }
        */
        
        //Optional在31章博客有点说明
    }
执行看看结果,然后我们继续补充:
@Test
    public void testSave(){

        Resume resume = new Resume();
        resume.setId(1l);
        resume.setName("让人人"); //其他没有写的,按照默认则是null
       //新增和更新都是使用Save方法的,通过是否有主键来区分(类设置了对应的注解的)
        Resume save = resumeDao.save(resume);//更新
        resume.setName("他人");
        System.out.println(save); //返回更新的数据,所以对上面的resume修改不会影响这个返回
        resume.setId(null);
        Resume save1 = resumeDao.save(resume);//新增
        resume.setName("和");
        System.out.println(save1); //返回要新增的数据,也就是上面的resume,所以上面的resume修改会影响这个返回

        //注意,在更新之前,或者更新方法执行后,会进行一次查询操作,来保证更新的数据存在
        //还有,如果查询的数据,与要更新的数据一样,是不会进行更新的,否则进行更新
        
        //上面的返回中,更新是创建对象得到对应的参数数据,然后返回,而新增则不是
        //因为考虑更新中有个查询(他只根据id查的),用这个新实例得到参数数据(如果一样的,那么不更新,如果不一样,就得到参数数据),然后进行更新
        
        //这里是可以返回对应的实例的,在mp中,通常只会考虑是否成功
    }
只所以会造成更新和新增的实例不同,根本原因还是方法的原因,可能在以后的版本中会有所改变,这里了解即可
上面的操作方式有点像mybatis-plus(mp),只是细节不同而已,当然了,mp通常不能自动的解决复杂sql(需要写sql,即半自动,虽然mp还有对应的依赖进行增强实现全自动,但那是其他依赖,不是mp自身存在的,了解即可),而jpa通常是可以的(即全自动,这里我们看后面)
如果考虑mp的增强实现全自动,那么对应的依赖加上mp(一般可以找到:mybatis-plus-join依赖)和jpa只是一种设计理念和操作方式不同的框架而已,具体使用谁,看自身情况
我们继续:
@Test
    public void testDelete(){
        resumeDao.deleteById(2l); //没有找到,也就是没有删除,就会报错
    }

我们继续:
 @Test
    public void testFindAll(){
        List<Resume> all = resumeDao.findAll(); //查询所有
        for (Resume resume : all) {
            System.out.println(resume);
        }
    }

  @Test
    public void testSort(){
        //设置id进行降序
        Sort sort = new Sort(Sort.Direction.DESC,"id"); //id是需要对应数据库的字段,他是在后面补充的,也可以说成是对应与注解,因为最终都是对应与数据库
        List<Resume> list = resumeDao.findAll(sort);
        for (int i = 0; i < list.size(); i++) {
            Resume resume = list.get(i);
            System.out.println(resume);
        }
    }


上面基本都是使用继承的接口的方法,我们也可以自定义一个:
回到前面的接口ResumeDao,在里面加上如下:
  //可以引⼊jpql(jpa查询语⾔)语句进⾏查询,jpql 语句类似于sql,只不过sql操作的是数据表和字段,jpql操作的是对象和属性
    //⽐如 from Resume where id=xx(在hibernate中称为hql语句)

    @Query("from Resume where id =?1") //指定?,代表对应的给入的值,后面的1代表接口参数中的第一个
    //即?1代表使用Long id
    public List<Resume> findByJpql(Long id);
我们回到测试类,然后加上如下:
 @Test
    public void testJpql() {
        List<Resume> byJpql = resumeDao.findByJpql(6l);
        for (Resume s : byJpql) {
            System.out.println(s);
        }
        /*
        Hibernate:
        select
        resume0_.id as id1_0_,
        resume0_.address as address2_0_,
        resume0_.name as name3_0_,
        resume0_.phone as phone4_0_
        from tb_resume resume0_
        where resume0_.id=?
Resume{id=6, name='让人', address='null', phone='null'}

对比一下这个:@Query("from Resume where id =?1")
很明显,这个注解是进行解析的,from Resume代表从这个类操作,这个类通常代表所在接口类对应继承的泛型来确定
后面的则是这个类对应的id的条件,和位置

还有,对应的接口可以不加返回值,只不过你不加的话,在扫描到这个注解时(对于dao层来说,对应的即有扫描,也有对应的直接读取,可以参考mybatis中对其dao的扫描操作(可以看看是否有注解),也可以参考106章博客)
进行执行时,自然也不会有返回值(反射),一般来说,如果考虑自动匹配一些返回值列表,通常需要代理(当然,代理的底层肯定是C完成),然后自行设置返回值

至此我们解释完毕
         */

    }
我们还可以继续补充参数:
 @Query("from Resume where id =?1 and name = ?2")
    public List<Resume> findByJpql(Long id,String name); //对应的Long id的id名称是可以随便写的
修改一下:
@Test
    public void testJpql() {
        List<Resume> byJpql = resumeDao.findByJpql(6l,"让人");
        for (Resume s : byJpql) {
            System.out.println(s);
        }


    }
进行测试吧,我们还可以操作原生的sql,我们在ResumeDao接口里面继续添加:
 //默认情况下nativeQuery = true为false,代表操作jpql,设置为true,代表操作原生的
    //如果操作原生的,自然需要完整的sql,否则自然报错(sql报错,导致我们报错)
    @Query(value = "select * from tb_resume where id =?1 and name = ?2",nativeQuery = true)
    public List<Resume> findBySql(Long id,String name);
继续测试:
  @Test
    public void testSql() {
        List<Resume> sql = resumeDao.findBySql(6l, "让人");
        for (Resume s : sql) {
            System.out.println(s);
        }


    }

注意:占位的基本都是解决了对应的sql注入,所以知道即可,还有我们这里还是建议使用原生的,这样不用框架来处理了,当然了,直接的使用原生的,可能只是对使用的那个数据库进行支持,并且不够动态(考虑修改类属性时,原生的没有提示),但是对操作更加复杂的sql有方便的处理
我们还可以这样:
在对应的ResumeDao接口中加上如下:
/*
    方法命名规则查询
     */
public List<Resume> findByNameLike(String name);
/*
select
resume0_.id as id1_0_,
resume0_.address as address2_0_,
resume0_.name as name3_0_,
resume0_.phone as phone4_0_
from tb_resume resume0_
where resume0_.name like ? escape ? 
单独参数且默认情况下(具体为什么添加,可能还有其他原因,这里了解即可),如果escape什么都不操作,可以认为是空值,如''(上面只是打印sql,具体后面的拼接在底层代码里面,所以并不是全部需要看我们的参数的,有些可能有自动的处理的)

 */

    public List<Resume> findByName(String name);
    /*
    select 
    resume0_.id as id1_0_, 
    resume0_.address as address2_0_, 
    resume0_.name as name3_0_, 
    resume0_.phone as phone4_0_ 
    from tb_resume resume0_ 
    where resume0_.name=?

     */
    
    //我们可以知道,对应的需要findBy开头,然后Name写上(首字母通常需要大写,否则报错,我测试了name,好像并不需要,但是还是建议大写,可能看版本),来确定操作谁,如果后面没有写具体的操作,如Like(首字母通常需要大写,否则报错),那么默认是等于的

我们进行测试:
@Test
    public void testMethodName(){
        List<Resume> resumes = resumeDao.findByNameLike("他%");
        for (Resume resume : resumes) {
            System.out.println(resume);
        }
        List<Resume> resumes1 = resumeDao.findByName("他");
        for (Resume resume : resumes1) {

            System.out.println(resume);
        }
    }

//也可以考虑
//findByNameAndAddress
//findByNameAndAddressLike
//findByNameAndLikeAddressLike(需要在后面,否则上面只是给Address操作Like)
//可以测试呗
当然了,如果存在注解,那么操作注解的,如果存在继承的,那么操作继承的
现在我们已经说明了四种:
/*
1:继承接口操作
2:使用jpql
3:原生sql
4:方法名称
*/
还有一种:动态查询,其中这里也是继承的(接口可以多继承,也可以说是第一种操作),我们看接口:
public interface ResumeDao extends JpaRepository<Resume, Long>,
        JpaSpecificationExecutor<Resume> {
            //..
            
        }
/*
在上面的JpaSpecificationExecutor中,可以看到如下:

*/
public interface JpaSpecificationExecutor<T> {
    //根据条件查询单个对象
    Optional<T> findOne(@Nullable Specification<T> var1);

    //根据条件查询所有
    List<T> findAll(@Nullable Specification<T> var1);

    //根据条件查询并进行分页
    Page<T> findAll(@Nullable Specification<T> var1, Pageable var2);

    //根据条件查询并进行排序
    List<T> findAll(@Nullable Specification<T> var1, Sort var2);

    //根据条件统计
    long count(@Nullable Specification<T> var1);
}
我们点开上面的Specification:
public interface Specification<T> extends Serializable {
    long serialVersionUID = 1L;

    static <T> Specification<T> not(Specification<T> spec) {
        return Specifications.negated(spec);
    }

    static <T> Specification<T> where(Specification<T> spec) {
        return Specifications.where(spec);
    }

    default Specification<T> and(Specification<T> other) {
        return Specifications.composed(this, other, CompositionType.AND);
    }

    default Specification<T> or(Specification<T> other) {
        return Specifications.composed(this, other, CompositionType.OR);
    }

    //用来封装条件的(一般只是查询的条件)
    //Root
    //CriteriaQuery
    //CriteriaBuilder
    @Nullable
    Predicate toPredicate(Root<T> var1, CriteriaQuery<?> var2, CriteriaBuilder var3);
}

那么我们直接在测试类中加上如下:
@Test
    public void testSpecification() {
        //动态条件封装
        Specification<Resume> objectSpecification = new Specification() {
            //root:需要查询的对象属性
            //criteriaBuilder:构建查询条件
            @Override
            public Predicate toPredicate(Root root, CriteriaQuery criteriaQuery, CriteriaBuilder criteriaBuilder) {

                //获取到name属性(会根据泛型找到对应类的)
                Path name = root.get("name");
                //构建条件
                Predicate predicate = criteriaBuilder.equal(name, "他");
                return predicate;
            }
        };
        /*
        select 
        resume0_.id as id1_0_, 
        resume0_.address as address2_0_, 
        resume0_.name as name3_0_, 
        resume0_.phone as phone4_0_ 
        from tb_resume resume0_ 
        where resume0_.name=?

         */
        //findOne只能让你查询一个,如果有多条数据,那么会报错
        Optional<Resume> one = resumeDao.findOne(objectSpecification);
        Resume resume = one.get(); //没有找到会报错的
        System.out.println(resume);
    }



继续测试一下:
@Test
    public void testSpecificationTest() {
        //动态条件封装
        Specification<Resume> objectSpecification = new Specification() {
            //root:需要查询的对象属性
            //criteriaBuilder:构建查询条件
            @Override
            public Predicate toPredicate(Root root, CriteriaQuery criteriaQuery, CriteriaBuilder criteriaBuilder) {

                //获取到name属性(会根据泛型找到对应类的)
                Path name = root.get("name");
                Path address = root.get("address");
                //构建条件
                Predicate predicate = criteriaBuilder.equal(name, "让人");
                Predicate predicate1 = criteriaBuilder.like(address.as(String.class), "分%");//告诉他,拼接的时候,是字符串
                //简单来说,如果address对应的类型不是String,那么转化成String,否则相当于不变(或者不写)

                //组合
                Predicate and = criteriaBuilder.and(predicate, predicate1);//有顺序的,predicate的在前面
                return and;
            }
        };
        /*
select
resume0_.id as id1_0_,
resume0_.address as address2_0_,
resume0_.name as name3_0_,
resume0_.phone as phone4_0_
from tb_resume resume0_
where resume0_.name=? and (resume0_.address like ?)



         */
        Optional<Resume> one = resumeDao.findOne(objectSpecification);
        Resume resume = one.get(); //没有找到会报错的
        System.out.println(resume);
    }
考虑一个问题,为什么要补充address.as(String.class),虽然说是变成字符串,会加上引号,但是在sql中,如果我们默认将所有数据都加上引号,不就行了,也就是无论是否是字符串都加上,为什么不这样处理,其实大多数框架或者其框架里面操作sql的部分通常都会考虑到是否添加,有些框架可能都加上单引号,有些可能为了严谨而不加上,而是看情况,大多数都是看情况的(如mybatis,通常考虑其底层逻辑的处理,如预处理对象:PreparedStatement,在41章博客有说明,mybatis内部一般是操作这个预处理对象的,然后操作数据库的),而这里通常也是,所以也可以不用设置as(String.class),具体可以自行测试
操作一下分页:
 @Test
    public void testPage() {
        /**
         * 第⼀个参数:当前查询的⻚数,可以从0开始,而不是1,从1开始代表是第二页
         * 由于是页,所以从0开始,默认为limit 0,2(也可以是limit 2,默认起始为0),那么第一页就是limit 2,2
         * 这里是页,而不是起始位置
         * 第⼆个参数:每⻚查询的数量
         */
        Pageable pageable = PageRequest.of(1, 2);
        //最终查询两个数量
        Page<Resume> all = resumeDao.findAll(pageable);
        System.out.println(all); //Page 2 of 5 containing com.pojo.Resume instances
        List<Resume> collect = all.get().collect(Collectors.toList());
        for (Resume resume : collect) {
            System.out.println(resume);
        }


        /*
        执行了两次,一个是分页的,一个是所有的数据
        select
        resume0_.id as id1_0_,
        resume0_.address as address2_0_,
        resume0_.name as name3_0_,
        resume0_.phone as phone4_0_
        from tb_resume resume0_ limit ?, ?

        select count(resume0_.id) as col_0_0_ from tb_resume resume0_

         */

        //上面Page<Resume> all = resumeDao.findAll(pageable);操作后,就会有上面两个sql的打印
        System.out.println(all.get().count());  //当前页的条数
        System.out.println(all.getTotalElements()); //总记录,select count(resume0_.id) as col_0_0_ from tb_resume resume0_
    }
Spring Data JPA 执行过程源码分析:
Spring Data Jpa 源码很少有人去分析,因为Spring Data Jpa 地位没有之前学习的框架高,习惯把它当成⼀个工具来用,并且接口的实现对象肯定是通过动态代理来完成的(也就是增强),且代理对象的产生过程追源码很难追,所以少有人分析
一般这个代理对象是这样的:

在这里插入图片描述

也就是这个类:
@Repository
@Transactional(
    readOnly = true
)
public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T, ID> {
    //..
    
}

@NoRepositoryBean
public interface JpaRepositoryImplementation<T, ID> extends JpaRepository<T, ID>, JpaSpecificationExecutor<T> {
    void setRepositoryMethodMetadata(CrudMethodMetadata var1);

    default void setEscapeCharacter(EscapeCharacter escapeCharacter) {
    }
}

public interface ResumeDao extends JpaRepository<Resume, Long>,
        JpaSpecificationExecutor<Resume> {
            
         //..   
        }

注意:上面不是代理对象的,是操作代理拦截后的返回的结果显示(当然,对应的调试显示也会因为是toString的结果,而这个toString的结果是代理接口的实现他的类的toString,如果有多个实现类,那么看你操作谁的方法了(创建代理中的指定的实现类的处理,如指定谁的实现接口,因为是匿名的,值已经得到了哦),就操作谁的toString),所以会出现对应的类,其接口的对象在上面的显示是:$Proxy39
所以说,对应的ResumeDao是代理对象,一般我们可以称这三个为代理对象,比如:
1:jdk代理生成的对象
2:jdk代理对象拦截后返回的对象(也就是上面的SimpleJpaRepository)
3:封装了jdk代理或者其他代理操作的对象
这三个都可以称为代理对象,只不过我们通常以第一个为主,所以如果在后面或者前面说明是代理对象时,应该就是这三个之中的
这里为了进行区分:我们称接口的对象是代理对象,其返回的对象是代理所产生的对象,那么ResumeDao就是代理对象
他使用什么代理呢,是JDK 动态代理
一般来说:在使用 JDK 动态代理时,每次调用 Proxy.newProxyInstance()方法都会创建一个新的代理对象,这些代理对象是由 JVM 在运行时动态生成的,并加载这些字节码来创建代理类的实例(也就是考虑jvm,或者java自身的处理字节码文件,并考虑使用类加载器的操作)
在 JDK 动态代理中,生成的代理类的名称是根据一定的规则生成的,通常遵循以下格式:
/*
比如:com.sun.proxy.$ProxyN(名称是$ProxyN,前面的包不确定,只是个例子)
比如上面的ResumeDao的对象就是$Proxy39

其中,N 是一个递增的数字,代表生成的代理类的索引号,每次创建新的代理对象时,这个索引号会递增,从而生成不同的代理类名称
例如,第一次调用 Proxy.newProxyInstance() 生成的代理类名称可能是 com.sun.proxy.$Proxy0,第二次是 com.sun.proxy.$Proxy1,以此类推,这些代理类名称都是由 JVM 自动生成的,是固定的格式,但具体的索引号会随着代理对象的创建而不断递增
那么可不可以设置名称:答,不可以
*/
当然,也可能存在有些框架不返回代理的可能,也就是说,在内部得到代理对象后,本质上又创建了一个实例返回给接口(考虑接口的子类),该子类他内部处理对应的代理对象,所以他本身不是代理对象,只不过间接的处理了代理对象而已
那么我们先拿取之前的代码:
  @Test
    public void testFindById() {
        Optional<Resume> optional = resumeDao.findById(1l);
        Resume resume = optional.get();
        System.out.println(resume);
    }

在这里进行分析吧
开始分析:
/*
打上断点
Optional<Resume> optional = resumeDao.findById(1l);



*/
首先我们需要找到产生的过程,一般由于他是Spring来赋值的,自然在Spring中进行处理,而Spring中存在AbstractApplicationContext的refresh方法中进行加载的,一般是这里(之前学习了Spring的底层原理,在那里可以找到):
public abstract class AbstractApplicationContext extends DefaultResourceLoader
		implements ConfigurableApplicationContext {
 //..   
}
/*
 第⼗一步:
 初始化所有剩下的⾮懒加载的单例bean
 初始化创建⾮懒加载方式的单例Bean实例(未设置属性)
 填充属性
 初始化方法调用(比如调用afterPropertiesSet方法、init-method方法)
 调用BeanPostProcessor(后置处理器)对实例bean进行后置处
 */
				finishBeanFactoryInitialization(beanFactory);
给上面打上断点,然后进行启动:
进入上面的方法:
public abstract class AbstractApplicationContext extends DefaultResourceLoader implements ConfigurableApplicationContext {
  
 //..
    
    //到这里(不同版本,显示不同,但是大致相同):
    protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) {
        if (beanFactory.containsBean("conversionService") && beanFactory.isTypeMatch("conversionService", ConversionService.class)) {
            beanFactory.setConversionService((ConversionService)beanFactory.getBean("conversionService", ConversionService.class));
        }

        if (!beanFactory.hasEmbeddedValueResolver()) {
            beanFactory.addEmbeddedValueResolver((strVal) -> {
                return this.getEnvironment().resolvePlaceholders(strVal);
            });
        }

        String[] weaverAwareNames = beanFactory.getBeanNamesForType(LoadTimeWeaverAware.class, false, false);
        String[] var3 = weaverAwareNames;
        int var4 = weaverAwareNames.length;

        for(int var5 = 0; var5 < var4; ++var5) {
            String weaverAwareName = var3[var5];
            this.getBean(weaverAwareName);
        }

        beanFactory.setTempClassLoader((ClassLoader)null);
        beanFactory.freezeConfiguration();
        //直接看这里:实例化所有立即加载的单例bean,我们进入这里
        beanFactory.preInstantiateSingletons();
    }
    
    //..
    
}
可以在上面进入后,看到这个:
List<String> beanNames = new ArrayList(this.beanDefinitionNames);
//他里面就存在对应的名称:resumeDao

//继续:
//我们可以到这里:
 if (this.isFactoryBean(beanName)) {
                        bean = this.getBean("&" + beanName);
                        break;
                    }
给上面的if (this.isFactoryBean(beanName)) {打上断点,然后点击这个:

在这里插入图片描述

只有满足条件的才是断点,所以到下一个断点就会到满足条件的那个地方,这个时候继续下一步:
 if (this.isFactoryBean(beanName)) { //是FactoryBean,那么存在对应的方法(getObject方法)得到实例
     //所以这个bean就是我们要的关键代码,我们进入看看
                        bean = this.getBean("&" + beanName);
                        break;
                    }
那么他是怎么配置成工厂bean的,或者说对应的名称是怎么处理工厂bean的,因为我们从来没有进行处理过,我们需要这对应的这个代码中打上断点:
 // 获取下一个bean名称
                            beanName = var2.next();
//RootBeanDefinition bd;
                            bd = this.getMergedLocalBeanDefinition(beanName);
/*
bd = this.getMergedLocalBeanDefinition(beanName); 是 Spring Framework 中的一个方法调用,用于获取给定 bean 名称的合并后的本地 Bean 定义(RootBeanDefinition),这个方法的主要作用是将 bean 定义中继承的属性、配置、父子关系等信息合并成一个完整的 bean 定义对象,便于后续实例化和管理
*/
一般情况下,被识别成工厂bean,通常在于实现类实现了对应的工厂接口,通常是:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.springframework.beans.factory;

import org.springframework.lang.Nullable;

public interface FactoryBean<T> {
    @Nullable
    T getObject() throws Exception;

    @Nullable
    Class<?> getObjectType();

    default boolean isSingleton() {
        return true;
    }
}

然后在xml中进行定义,创建对象实例,然后判断,当然,并不是必须要xml,因为这里是整合的,所以对应的大概率是注解生成
即对应的代理对象,应该是读取某个地方的注解,生成实例,通过判断是否实现了对应的接口FactoryBean,然后操作getObject返回实例,该返回的实例就是代理对象
但是中间也操作了bd = this.getMergedLocalBeanDefinition(beanName);,他干什么了,我们先看在名称为resumeDao时得到的值的显示:
Root bean: class [org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean]; scope=singleton; abstract=false; lazyInit=false; autowireMode=0; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=null; factoryMethodName=null; initMethodName=null; destroyMethodName=null
也就是:JpaRepositoryFactoryBean,虽然他是RootBeanDefinition类型的,但是对应的这个肯定与他有关系
我们进入bd = this.getMergedLocalBeanDefinition(beanName);(记得调试条件):
public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFactory implements ConfigurableListableBeanFactory, BeanDefinitionRegistry, Serializable {
 //..
}

public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFactory implements AutowireCapableBeanFactory {
 //..
}

public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport implements ConfigurableBeanFactory { //ConfigurableListableBeanFactory
    //this是:DefaultListableBeanFactory对象,ConfigurableListableBeanFactory得到(DefaultListableBeanFactory的父类)
    /*
    public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFactory implements ConfigurableListableBeanFactory, BeanDefinitionRegistry, Serializable {

    */
  
 //..
    
    protected RootBeanDefinition getMergedLocalBeanDefinition(String beanName) throws BeansException {
        //private final Map<String, RootBeanDefinition> mergedBeanDefinitions = new ConcurrentHashMap(256);
   
        //从集合里面拿取,拿取到了对应的RootBeanDefinition
        
        RootBeanDefinition mbd = (RootBeanDefinition)this.mergedBeanDefinitions.get(beanName);
        return mbd != null ? mbd : this.getMergedBeanDefinition(beanName, this.getBeanDefinition(beanName));
    }
    
    //..
    
}
既然从集合里面拿取,那么什么时候放入的:
可以到关于this的进行全局搜索,一般就在AbstractBeanFactory里面可以找到mergedBeanDefinitions.put,发现在:
   protected RootBeanDefinition getMergedBeanDefinition(String beanName, BeanDefinition bd, @Nullable BeanDefinition containingBd) throws BeanDefinitionStoreException {
       //..
        synchronized(this.mergedBeanDefinitions) {
            RootBeanDefinition mbd = null;
            if (containingBd == null) {
                mbd = (RootBeanDefinition)this.mergedBeanDefinitions.get(beanName);
            }
   //..
   }
在上面方法的里面,我们给RootBeanDefinition mbd = null;,加上断点,发现有String beanName,那么继续操作条件
当然,有很多操作会操作到这里,我们只看他进行第一次put的情况,我们继续观察,这个时候可以发现BeanDefinition bd中bd是有值的,所以需要看他调用栈来确定了,看这里:

在这里插入图片描述

我们进入方法:
 public BeanDefinition getBeanDefinition(String beanName) throws NoSuchBeanDefinitionException {
     //也就是这个地方:beanDefinitionMap是存放bean信息的工厂,从里面拿取对应的bean信息(比如名称和全限定名称)
     BeanDefinition bd = (BeanDefinition)this.beanDefinitionMap.get(beanName);
        if (bd == null) {
            if (this.logger.isTraceEnabled()) {
                this.logger.trace("No bean named '" + beanName + "' found in " + this);
            }

            throw new NoSuchBeanDefinitionException(beanName);
        } else {
            return bd;
        }
    }

我们找他put方法:
//就在当前类里面可以找到
public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition) throws BeanDefinitionStoreException {
    
    //..
     Assert.hasText(beanName, "Bean name must not be empty");
        Assert.notNull(beanDefinition, "BeanDefinition must not be null");
        if (beanDefinition instanceof AbstractBeanDefinition) {
            try {
                ((AbstractBeanDefinition)beanDefinition).validate();
            } catch (BeanDefinitionValidationException var8) {
                throw new BeanDefinitionStoreException(beanDefinition.getResourceDescription(), beanName, "Validation of bean definition failed", var8);
            }
        }

 //..   
}
     
在上面的 Assert.notNull(beanDefinition, “BeanDefinition must not be null”);打上断点并加上条件(有beanName),看调用栈:
我们看参数beanDefinition有值,且放入的map中就是他,那么看看调用栈中是谁先没有的:

在这里插入图片描述

我们继续看后面的调用栈:

在这里插入图片描述

上面考虑解析我们的自定义标签,一般来说,由于我们的标签存在自定义的,这里也就是jpa标签的,具体解析也由jpa来完成(spring可没有全部的解析过程,或者说解析器哦,这就需要框架来完成了),这里说明的标签一般就是:jpa:repositories
我们进入看看方法:
public class RepositoryConfigurationDelegate {
    
 //..
     public List<BeanComponentDefinition> registerRepositoriesIn(BeanDefinitionRegistry registry, RepositoryConfigurationExtension extension) {
        if (LOG.isInfoEnabled()) {
            LOG.info("Bootstrapping Spring Data repositories in {} mode.", this.configurationSource.getBootstrapMode().name());
        }

        extension.registerBeansForRoot(registry, this.configurationSource);
        RepositoryBeanDefinitionBuilder builder = new RepositoryBeanDefinitionBuilder(registry, extension, this.configurationSource, this.resourceLoader, this.environment);
        List<BeanComponentDefinition> definitions = new ArrayList();
        StopWatch watch = new StopWatch();
        if (LOG.isDebugEnabled()) {
            LOG.debug("Scanning for repositories in packages {}.", this.configurationSource.getBasePackages().stream().collect(Collectors.joining(", ")));
        }

        watch.start();
        Collection<RepositoryConfiguration<RepositoryConfigurationSource>> configurations = extension.getRepositoryConfigurations(this.configurationSource, this.resourceLoader, this.inMultiStoreMode);
        Map<String, RepositoryConfiguration<?>> configurationsByRepositoryName = new HashMap(configurations.size());
        Iterator var8 = configurations.iterator();

        while(var8.hasNext()) {
            RepositoryConfiguration<? extends RepositoryConfigurationSource> configuration = (RepositoryConfiguration)var8.next();
            configurationsByRepositoryName.put(configuration.getRepositoryInterface(), configuration);
            //到这里看看
            BeanDefinitionBuilder definitionBuilder = builder.build(configuration);
            extension.postProcess(definitionBuilder, this.configurationSource);
            if (this.isXml) {
                extension.postProcess(definitionBuilder, (XmlRepositoryConfigurationSource)this.configurationSource);
            } else {
                extension.postProcess(definitionBuilder, (AnnotationRepositoryConfigurationSource)this.configurationSource);
            }

            //definitionBuilder是直接得到的,所以考虑前面
            AbstractBeanDefinition beanDefinition = definitionBuilder.getBeanDefinition();
            String beanName = this.configurationSource.generateBeanName(beanDefinition);
            if (LOG.isTraceEnabled()) {
                LOG.trace("Spring Data {} - Registering repository: {} - Interface: {} - Factory: {}", new Object[]{extension.getModuleName(), beanName, configuration.getRepositoryInterface(), configuration.getRepositoryFactoryBeanClassName()});
            }

            beanDefinition.setAttribute("factoryBeanObjectType", configuration.getRepositoryInterface());
            registry.registerBeanDefinition(beanName, beanDefinition);
            definitions.add(new BeanComponentDefinition(beanDefinition, beanName));
        }

    //..
}
给上面的BeanDefinitionBuilder definitionBuilder = builder.build(configuration);打上断点,不需要条件(因为没不知道实使用什么条件)
//可以到这里:
  BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(configuration.getRepositoryFactoryBeanClassName());
       
public final class BeanDefinitionBuilder {
      private final AbstractBeanDefinition beanDefinition; //这个beanDefinition的值就是:JpaRepositoryFactoryBean了,我们找到最终的地方了
 //..   
}
       
public abstract class AbstractBeanDefinition extends BeanMetadataAttributeAccessor implements BeanDefinition, Cloneable {
 
 //..   
}
public class RootBeanDefinition extends AbstractBeanDefinition {
 //..   
}
然后我们进入configuration.getRepositoryFactoryBeanClassName():
public class DefaultRepositoryConfiguration<T extends RepositoryConfigurationSource> implements RepositoryConfiguration<T> {

 //..
    public String getRepositoryFactoryBeanClassName() {
        return (String)this.configurationSource.getRepositoryFactoryBeanClassName().orElseGet(() -> {
            return this.extension.getRepositoryFactoryBeanClassName();
        });
    }
    //..
}
他是一个遍历,操作自定义的,所以最终我们会得到自定义的标签处理(考虑扫描的),最终也就是:
public String getRepositoryFactoryBeanClassName() {
        return JpaRepositoryFactoryBean.class.getName(); //最终的返回了
    }
得到自定义标签处理了,最终把这个返回,放入到对应的map中,也就是注册过程中进行了得到(自然也就会考虑名称),然后最终被我们得到,即:
BeanDefinition bd = (BeanDefinition)this.beanDefinitionMap.get(beanName);
这样我们就得到了bean的信息了
简单来说:我们自定义标签后,spring让我们自行处理自定义的标签,我们的处理就是读取自定义标签信息,如包扫描,然后扫描到对应的接口,然后自定义一个实例信息用来进行创建实例,也就是说,对应的信息保存的不是接口或者类的全限定名,而是自定义或者说固定的,这里也就是:JpaRepositoryFactoryBean,那么根据这样的说明,本质上spring和mybatis的整合自然也是如此,当然了,单独的mybatis由于没有像spring的信息操作,所以他是单纯的搞个代理,而不是考虑spring信息的统一处理的,然后考虑代理,因为spring需要考虑很多地方的,而不是mybatis的单独一个
那么我们可以回到之前的这个地方了:
bd = this.getMergedLocalBeanDefinition(beanName);
也就是说,他拿取的是我们固定的信息,即JpaRepositoryFactoryBean,操作实例时,自然考虑操作他,所以在后面的操作bean时(spring底层原理中有部分说明),会使用JpaRepositoryFactoryBean来操作创建bean,且判断是否为工厂对象也是判断他的,我们看这个:
public class JpaRepositoryFactoryBean<T extends Repository<S, ID>, S, ID> extends TransactionalRepositoryFactoryBeanSupport<T, S, ID> {
 //..
    
   
    
}

public abstract class TransactionalRepositoryFactoryBeanSupport<T extends Repository<S, ID>, S, ID> extends RepositoryFactoryBeanSupport<T, S, ID> implements BeanFactoryAware {
 //..
}

public abstract class RepositoryFactoryBeanSupport<T extends Repository<S, ID>, S, ID> implements InitializingBean, RepositoryFactoryInformation<S, ID>, FactoryBean<T>, BeanClassLoaderAware, BeanFactoryAware, ApplicationEventPublisherAware {
 
 //..   
}

//好吧,他有FactoryBean
//那么他的确是工厂对象



既然确定了得到信息的过程,那么在确定他是工厂的情况下,我们找到他的getObject方法,一般在他的父类的:

在这里插入图片描述

上面选择的是Show Inherited,代表显示继承的,一般有这个就可以全部看到了(因为实现的必须要自行写上的)
或者在电脑上按:ctrl+F12(如果是笔记本,可能需要加上fn来让F12生效)

在这里插入图片描述

我们进入:
public class JpaRepositoryFactoryBean<T extends Repository<S, ID>, S, ID> extends TransactionalRepositoryFactoryBeanSupport<T, S, ID> {
 
 //..   
}

public abstract class TransactionalRepositoryFactoryBeanSupport<T extends Repository<S, ID>, S, ID> extends RepositoryFactoryBeanSupport<T, S, ID> implements BeanFactoryAware {
//..
}

public abstract class RepositoryFactoryBeanSupport<T extends Repository<S, ID>, S, ID> implements InitializingBean, RepositoryFactoryInformation<S, ID>, FactoryBean<T>, BeanClassLoaderAware, BeanFactoryAware, ApplicationEventPublisherAware {
    
 //..
    @Nonnull
    public T getObject() {
        return (Repository)this.repository.get();
    }
    
    //..
    
    //这里是赋值this.repository的地方
     public void afterPropertiesSet() {
        this.factory = this.createRepositoryFactory();
        this.factory.setQueryLookupStrategyKey(this.queryLookupStrategyKey);
        this.factory.setNamedQueries(this.namedQueries);
        this.factory.setEvaluationContextProvider((QueryMethodEvaluationContextProvider)this.evaluationContextProvider.orElseGet(() -> {
            return QueryMethodEvaluationContextProvider.DEFAULT;
        }));
        this.factory.setBeanClassLoader(this.classLoader);
        this.factory.setBeanFactory(this.beanFactory);
        if (this.publisher != null) {
            this.factory.addRepositoryProxyPostProcessor(new EventPublishingRepositoryProxyPostProcessor(this.publisher));
        }

        RepositoryFactorySupport var10001 = this.factory;
        this.repositoryBaseClass.ifPresent(var10001::setRepositoryBaseClass);
        RepositoryFragments customImplementationFragment = (RepositoryFragments)this.customImplementation.map((xva$0) -> {
            return RepositoryFragments.just(new Object[]{xva$0});
        }).orElseGet(RepositoryFragments::empty);
        RepositoryFragments repositoryFragmentsToUse = ((RepositoryFragments)this.repositoryFragments.orElseGet(RepositoryFragments::empty)).append(customImplementationFragment);
        this.repositoryMetadata = this.factory.getRepositoryMetadata(this.repositoryInterface);
        this.mappingContext.ifPresent((it) -> {
            it.getPersistentEntity(this.repositoryMetadata.getDomainType());
        });
         
         //这里被赋值了
        this.repository = Lazy.of(() -> {
            return (Repository)this.factory.getRepository(this.repositoryInterface, repositoryFragmentsToUse);
        });
        if (!this.lazyInit) {
            this.repository.get();
        }

    }
    
    //..
}
 
他为什么可以执行afterPropertiesSet方法,因为他实现了InitializingBean接口,在spring中在实例放入map前面需要实现一系列的方法,所以这里会实现,即:调用InitializingBean接口的afterPropertiesSet方法,所以会进行设置(提一下:spring初始化并不是构造方法,只是一个单纯来执行的方法)
我们给上面的这个打上断点:
this.repository = Lazy.of(() -> {
            return (Repository)this.factory.getRepository(this.repositoryInterface, repositoryFragmentsToUse);
        });
直接进入到这里(是下面的,看清楚,记得跳出不相关的,以后和以前可能没有说明,这里说明一下,后面不说明了):
public abstract class RepositoryFactorySupport implements BeanClassLoaderAware, BeanFactoryAware {
 
    //..
   public <T> T getRepository(Class<T> repositoryInterface, RepositoryFragments fragments) {
       //如果调试日志启用,则记录初始化仓库实例的信息
        if (LOG.isDebugEnabled()) {
            LOG.debug("Initializing repository instance for {}…", repositoryInterface.getName());
        }

       // 断言仓库接口和仓库片段不能为空
        Assert.notNull(repositoryInterface, "Repository interface must not be null!");
        Assert.notNull(fragments, "RepositoryFragments must not be null!");
       // 获取仓库元数据
        RepositoryMetadata metadata = this.getRepositoryMetadata(repositoryInterface);
       // 获取仓库组合
        RepositoryComposition composition = this.getRepositoryComposition(metadata, fragments);
       // 获取仓库信息
        RepositoryInformation information = this.getRepositoryInformation(metadata, composition);
       // 验证仓库信息和仓库组合
        this.validate(information, composition);
       // 获取目标仓库实例
        Object target = this.getTargetRepository(information);
       // 创建动态代理工厂
        ProxyFactory result = new ProxyFactory();
       // 设置目标仓库实例为代理的目标
        result.setTarget(target);
       // 设置代理的接口,包括仓库接口、Repository 接口和 TransactionalProxy 接口
        result.setInterfaces(new Class[]{repositoryInterface, Repository.class, TransactionalProxy.class});
       // 如果支持仓库接口的方法验证,则添加方法验证器
        if (MethodInvocationValidator.supports(repositoryInterface)) {
            result.addAdvice(new MethodInvocationValidator());
        }

       // 添加围绕事务的拦截器
        result.addAdvice(SurroundingTransactionDetectorMethodInterceptor.INSTANCE);
       // 添加暴露调用拦截器
        result.addAdvisor(ExposeInvocationInterceptor.ADVISOR);
        // 对代理进行后处理
        this.postProcessors.forEach((processor) -> {
            processor.postProcess(result, information);
        });
       // 添加默认方法调用的方法拦截器
        result.addAdvice(new DefaultMethodInvokingMethodInterceptor());
       // 获取投影工厂
        ProjectionFactory projectionFactory = this.getProjectionFactory(this.classLoader, this.beanFactory);
        // 添加查询执行方法的拦截器
        result.addAdvice(new RepositoryFactorySupport.QueryExecutorMethodInterceptor(information, projectionFactory));
       // 将仓库组合追加为已实现的仓库片段
        composition = composition.append(RepositoryFragment.implemented(target));
       // 添加实现方法执行拦截器
        result.addAdvice(new RepositoryFactorySupport.ImplementationMethodExecutionInterceptor(composition));
       // 生成代理对象
        T repository = result.getProxy(this.classLoader);
       // 如果调试日志启用,则记录完成仓库实例的创建
        if (LOG.isDebugEnabled()) {
            LOG.debug("Finished creation of repository instance for {}.", repositoryInterface.getName());
        }

       // 返回仓库实例
        return repository;
    }

    //..
    
}
当然,在上面打上断点也可以,在这里:RepositoryInformation information = this.getRepositoryInformation(metadata, composition);
我们看看调试信息:

在这里插入图片描述

发现了没有,存在了SimpleJpaRepository:
public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T, ID> {
//..
}

也就是说,我们看到了对应在前面的代理类里面的信息,即SimpleJpaRepository,我们看看他怎么生成的
 RepositoryInformation information = this.getRepositoryInformation(metadata, composition);
      
//进入:
//在里面的Class<?> baseClass = (Class)this.repositoryBaseClass.orElse(this.getRepositoryBaseClass(metadata));中就会得到这个结果

protected Class<?> getRepositoryBaseClass(RepositoryMetadata metadata) {
        return SimpleJpaRepository.class;
    }
//而且还是固定的指定
然后借助这个:
// 创建动态代理工厂
        ProxyFactory result = new ProxyFactory();
来产生工厂,中间进行一些设置,有传递包含SimpleJpaRepository的信息,然后到这里:
// 生成代理对象
        T repository = result.getProxy(this.classLoader);

//进入:
他是最后的返回的,然后让对应的:
@Nonnull
    public T getObject() {
        return (Repository)this.repository.get();
    }
    
这个来获取
那么我们进入前面的result.getProxy(this.classLoader);:
 public Object getProxy(@Nullable ClassLoader classLoader) {
        return this.createAopProxy().getProxy(classLoader);
    }

 protected final synchronized AopProxy createAopProxy() {
        if (!this.active) {
            this.activate();
        }

     //进入
        return this.getAopProxyFactory().createAopProxy(this);
    }


public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
        if (!config.isOptimize() && !config.isProxyTargetClass() && !this.hasNoUserSuppliedProxyInterfaces(config)) {
            return new JdkDynamicAopProxy(config);
        } else {
            Class<?> targetClass = config.getTargetClass();
            if (targetClass == null) {
                throw new AopConfigException("TargetSource cannot determine target class: Either an interface or a target is required for proxy creation.");
            } else {
                
                //看这里,可以发现对应是操作代理的
                return (AopProxy)(!targetClass.isInterface() && !Proxy.isProxyClass(targetClass) ? new ObjenesisCglibAopProxy(config) : new JdkDynamicAopProxy(config));
            }
        }
    }

经过调试,对应的是jdk动态代理,并且他是操作new JdkDynamicAopProxy(config),他是代理对象工厂,我们继续回到之前的:
public Object getProxy(@Nullable ClassLoader classLoader) {
        return this.createAopProxy().getProxy(classLoader);
    }

这次我们进入getProxy,也就是JdkDynamicAopProxy的getProxy方法:
  public Object getProxy(@Nullable ClassLoader classLoader) {
        if (logger.isTraceEnabled()) {
            logger.trace("Creating JDK dynamic proxy: " + this.advised.getTargetSource());
        }

        Class<?>[] proxiedInterfaces = AopProxyUtils.completeProxiedInterfaces(this.advised, true);
        this.findDefinedEqualsAndHashCodeMethods(proxiedInterfaces);
      //创建代理对象了吧
        return Proxy.newProxyInstance(classLoader, proxiedInterfaces, this);
    }
最后我们返回,我们看看他的信息:

在这里插入图片描述

返回后的信息:

在这里插入图片描述

在代理中,对应的显示后面是toString的结果,但是也要注意:这通常是考虑invoke里面的,匿名得到的值(这很明显,基本是固定的返回),还有返回的这个代理,而h,则代表是使用那个类来完成这个创建的(相当于this),这里很明显是使用对应的工厂类,后面就是里面的一些设置的信息了(这些信息了解即可)
而拦截得到的结果是上面的return Proxy.newProxyInstance(classLoader, proxiedInterfaces, this);的this来处理的,那么他当前类肯定实现了InvocationHandler接口,即里面必然操作了invoke(不愧是代理工厂),里面肯定会返回对应的操作
至此:我们得到的T repository = result.getProxy(this.classLoader);中,就是一个拦截后,得到结果,注意是拦截后,也就是说,不拦截的情况下他自然是代理对象
最后对应的getObject就会得到这个值,因为:
 this.repository = Lazy.of(() -> {
            return (Repository)this.factory.getRepository(this.repositoryInterface, repositoryFragmentsToUse);
        });

上面设置了value属性,在getObject中:
return (Repository)this.repository.get();

 public T get() {
        T value = this.getNullable();
        if (value == null) {
            throw new IllegalStateException("Expected lazy evaluation to yield a non-null value but got null!");
        } else {
            return value;
        }
    }

@Nullable
    private T getNullable() {
        T value = this.value;
        if (this.resolved) {
            return value;
        } else {
            value = this.supplier.get();
            this.value = value;
            this.resolved = true;
            return value;
        }
    }
就返回了这个属性,所以最终得到了对应的T repository = result.getProxy(this.classLoader);
也就是说,通过getObject生成的代理对象交给ResumeDao接口赋值,然后这个接口作为代理对象的引用,在调用对应的方法时,将拦截产生的结果进行赋值,最终我们得到了对应的处理,如查询,新增,删除,修改等等
@Repository
@Transactional(
    readOnly = true
)
public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T, ID> {
    //..
    
}

@NoRepositoryBean
public interface JpaRepositoryImplementation<T, ID> extends JpaRepository<T, ID>, JpaSpecificationExecutor<T> {
    void setRepositoryMethodMetadata(CrudMethodMetadata var1);

    default void setEscapeCharacter(EscapeCharacter escapeCharacter) {
    }
}

//正好对应与ResumeDao的接口信息
public interface ResumeDao extends JpaRepository<Resume, Long>,
        JpaSpecificationExecutor<Resume> {
            
         //..   
        }

当然,其ResumeDao接口里面自定义的接口方法,通常在前面spring让我们操作自定义标签时,会对其实例进行一些处理,当操作的是对应自身的接口方法时,对应的拦截产生的对象中,可能在产生过程会进行一些处理,使得SimpleJpaRepository可能会进行一些处理,在mybatis中是找到xml,读取sql,进行执行处理,将结果返回,然后我们得到,那么这里是继续读取名称或者注解,解析成sql后(自定义的部分,而不是其他的固定部分),执行sql,返回结果,即最终操作拦截后的返回,这样我们就得到了对应的查询,删除,修改,新增的结果了
但是考虑到返回结果的toString是SimpleJpaRepository的类型,那么大概率是在执行invoke方法时,传递的就是这个类,而这个类就实现了上面的对应方法,来完成拦截,如果是自定义的,那么可能是将sql作为参数传递,或者执行一些其他的方法,让该类进行处理得到结果,这里可以参照106章博客(因为在107章博客或者后面中只考虑增强,没有考虑返回,虽然mybatis的底层原理(106章博客)中考虑的返回是直接的结果,而非交给其他类执行(这里就是交给SimpleJpaRepository处理))
至此我们的Spring Data JPA源码解析说明完毕了,以后需要深入的时候,看看其他如注解的处理吧(前面的也是,如mybatis,spring,springmvc等等,都只是说明,而非深入)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/716895.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

从手动到自动再到智能,IT运维的进击之路

过去十年&#xff0c;科技化进程飞速发展&#xff0c;作为保障企业信息安全运行的运维行业&#xff0c;从手动发展到自动&#xff0c;又从自动发展到智能&#xff0c;IT运维也从原来的辅助角色逐渐变成了业务推动。 手动——自动——智能的发展 以银行用户为例&#xff0c;最初…

月球全月地质图和4.5亿像素月面标注地图

嫦娥六号都在月球挖到土特产了&#xff0c;那你知道月球到底长什么样子吗&#xff1f; 现在我们就为你分享一下月球的全月地质图&#xff0c;以及4.5亿像素月面带标注的地图&#xff0c;你可以在文末查看该数据的领取方法。 月球全月地质图 对于月球的探索&#xff0c;美国和…

OAuth 2.0:现代应用程序的授权标准

前言 随着互联网和移动应用的发展&#xff0c;应用程序之间的交互变得越来越普遍。用户希望通过单一的身份认证在多个平台上无缝体验&#xff0c;这就要求不同的应用程序能够安全地共享用户数据。而 OAuth 2.0 正是为了解决这一问题而设计的&#xff0c;它提供了一种标准机制&…

AI绘画界的赛博佛祖,开源最强SD3它来了!(整合包)

全网期待已久的SD3终于和大家见面了。这款以Stable Diffusion为基础&#xff0c;进一步优化和升级的模型&#xff0c;无疑将会深刻地又又又一次改变AI绘画界&#xff01; 这次发布的是Medium版本&#xff0c;在多个方面展现出惊人的能力和效率&#xff0c;堪称开源最强&#x…

[Python学习篇] Python列表

列表&#xff08;List&#xff09;&#xff1a;列表是可变的&#xff0c;这意味着你可以修改列表的内容&#xff0c;例如增加、删除或更改元素。列表使用方括号 [] 表示。列表可以一次性存储多个数据&#xff0c;且可以存不同数据类型。 语法&#xff1a; [数据1, 数据2, 数据3…

数字电路运算器分析

文章目录 1. 半加器 2. 加法器 3. 4位加法器 4. 半减器 5. 减法器 6. 4位减法器 1. 半加器 现在我们来考虑如何用电路来实现1位加法。假如有两个1位二进制数A、B&#xff0c;它们的和为1位二进制数S&#xff0c;那么存在下面几种情况&#xff1a; 如果A0&#xff0c;B…

ensp模拟器USG6000V1配置DCHP功能

接着上一篇配置&#xff0c;继续本篇的内容。开启DHCP功能非常简单&#xff0c;只需几个命令即可。实验拓扑图也非常简单&#xff0c;如下&#xff1a; 开启防火墙DHCP功能&#xff1a; [USG6000V1]dhcp enable 选择DHCP接口并设置接口IP地址&#xff0c;这里给g1/0/0配置2网…

【华为免费实战课】基于ENSP实现企业园区网组网项目实战

带你一起走进网工的世界&#xff01; 2024年G-LAB【华为实战公开课】即将开始啦&#xff01;华为实战千万别错过&#xff01; 公开课为期四天&#xff0c;6月18日-6月21日晚20&#xff1a;00开始 关注 工 仲 好&#xff1a;IT运维大本营&#xff0c;私信glab-mary&#xff0…

概率论拾遗

条件期望的性质 1.看成f(Y)即可 条件期望仅限于形式化公式&#xff0c;用于解决多个随机变量存在时的期望问题求解&#xff0c;即 E(?)E(E(?|Y))#直接应用此公式条件住一个随机变量&#xff0c;进行接下来的计算即可 定义随机变量之间的距离为&#xff0c;即均方距离 随机…

Go基础编程 - 09 - 通道(channel)

通道&#xff08;channel&#xff09; 1. 声明2. channel的操作3. 无缓冲通道4. 有缓冲通道5. 如何优雅的从通道循环取值6. 单向通道7. 异常总结 上一篇&#xff1a;结构体 Go语言的并发模式&#xff1a;不要通过共享内存来通信&#xff0c;而应该通过通信来共享内存。 Go语言…

cesium ClippingPolygon多边形裁切

1.多边形裁切 1.1 基本流程 cesium117版本添加了多边形裁切功能&#xff0c;本文分析源码&#xff0c;看看是如何处理的。多边形裁切的大概流程分为4部分: 通过经纬度坐标传入多个闭合的边界&#xff1b;将多个边界打包成两张纹理&#xff0c;一张是每个多边形的坐标&#xf…

Spring框架永远滴神之SpringAI玩转大模型

文章目录 一、SpringAI简介1.什么是SpringAI2.SpringAI支持的大模型类型&#xff08;1&#xff09;聊天模型&#xff08;2&#xff09;文本到图像模型&#xff08;3&#xff09;转录&#xff08;音频到文本&#xff09;模型&#xff08;4&#xff09;嵌入模型&#xff08;5&…

多标签识别:JoyTag模型的图像标注革命【开源】

公共视觉模型通常会对其训练数据集进行严格过滤&#xff0c;这限制了这些基础模型在广泛概念上的表现&#xff0c;进而限制了表达自由、包容性和多样性。JoyTag通过结合Danbooru 2021数据集和一组手动标记的图像&#xff0c;努力提高模型对不同类型图像的泛化能力。 JoyTag项目…

Python批量保存Excel文件中的图表为图片

Excel工作簿作为一款功能强大的数据处理与分析工具&#xff0c;被广泛应用于各种领域&#xff0c;不仅能够方便地组织和计算数据&#xff0c;还支持用户创建丰富多彩的图表&#xff0c;直观展示数据背后的洞察与趋势。然而&#xff0c;在报告编制、网页内容制作或分享数据分析成…

新办理北京广播电视节目制作许可证需要什么条件

在北京想要从事广播电视节目制作&#xff0c;那就需要企业拥有广播电视节目制作经营许可证。此许可证不仅是企业合法经营的基础&#xff0c;同时也是保障节目制作质量和内容合规的标志。如何办理&#xff0c;详情致电咨询我或者来公司面谈。 北京广播电视节目制作经营许可证申请…

开源项目大合集(热门)

人不走空 &#x1f308;个人主页&#xff1a;人不走空 &#x1f496;系列专栏&#xff1a;算法专题 ⏰诗词歌赋&#xff1a;斯是陋室&#xff0c;惟吾德馨 目录 &#x1f308;个人主页&#xff1a;人不走空 &#x1f496;系列专栏&#xff1a;算法专题 ⏰诗词歌…

【Python】PySide6使用入门和注意事项

文章目录 前言关于PySide和PyQtQt Designerpyside6在vscode中ui文件转换兼容性问题主程序结构蓝牙协议初探&#xff08;应用层&#xff09; 前言 最近在开发一个带界面的软件&#xff0c;需要使用蓝牙&#xff0c;然后找到一个开源仓库使用的是Qt里面的Qbluetooth模块&#xff…

「网络原理」IP 协议

&#x1f387;个人主页&#xff1a;Ice_Sugar_7 &#x1f387;所属专栏&#xff1a;计网 &#x1f387;欢迎点赞收藏加关注哦&#xff01; IP 协议 &#x1f349;报头结构&#x1f349;地址管理&#x1f34c;动态分配 IP 地址&#x1f34c;NAT 机制&#xff08;网络地址映射&am…

AMD平台,5600X+6650XT,虚拟机安装macOS 14(2024年6月)

AMD平台安装macOS 14的麻烦&#xff0c;要比Intel平台多的多&#xff0c;由于macOS从13开始&#xff0c;对CPU寄存器的读取进行了改变&#xff0c;导致AMD平台只要安装完macOS 13及以后版本&#xff0c;开机后就报五国语言错误&#xff0c;不断重启。改vmx文件&#xff0c;被证…

VR虚拟仿真技术模拟还原给水厂内外部结构

在厂区的外围&#xff0c;我们采用VR全景拍摄加3D开发建模的方式&#xff0c;还原了每一处细节&#xff0c;让你仿佛置身于现场&#xff0c;感受那份宁静与庄重。 当你踏入厂区&#xff0c;我们为你精心策划了一条游览路线&#xff0c;从门口到各个重要场景&#xff0c;一一为…