javassist 字节码处理库

目录

  • 一、快速入门
    • 1.1 创建class文件
    • 1.2 ClassPool的相关方法
    • 1.3 CtClass的相关方法
    • 1.4 CtMethod的相关方法
    • 1.5 调用生成的类对象
      • 1.5.1 通过反射调用
      • 1.5.2 通过接口调用
    • 1.6 修改现有的类对象
  • 二、将类冻结
  • 三、类搜索路径
  • 四、$开头的特殊字符
  • 五、ProxyFactory的使用

我们知道Java字节码以二进制的形式存储在class文件中,每一个class文件包含一个Java类或接口。Javaassist 就是一个用来处理Java字节码的类库。在Javassist 中,类CtClass表示class文件。

我们可以用javassist类库实现动态创建类、添加类的属性和方法、设置类的父类,以及修改类的方法等操作。Javassist不允许删除方法或字段,但它允许更改名称。所以,如果一个方法是没有必要的,可以通过调用CtMethod的setName和setModifiers中将其改为一个私有方法。Javassist不允许向现有方法添加额外的参数。你可以通过新建一个方法达到同样的效果。

使用前需要先引入javassist库

以gradle工程为例,添加如下依赖:

 implementation 'org.javassist:javassist:3.28.0-GA'

一、快速入门

1.1 创建class文件

假设我们需要在当前工程的/out/class目录内创建一个Person.class文件,例如:
在这里插入图片描述
idea反编译查看的源码内容如下:

package com.test;

public class Person {
private String name = "小明";
private int age = 20;

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

public String getName() {
    return this.name;
}

public void setAge(int var1) {
    this.age = var1;
}

public int getAge() {
    return this.age;
}

public Person() {
    this.name = "老王";
    this.age = 30;
}

public Person(String var1, int var2) {
    this.name = var1;
    this.age = var2;
}

public void printName() {
    System.out.println(this.name);
}

public void printAge() {
    System.out.println(this.age);
}
}

使用javassist可以很方便的实现:

package com.demo

import javassist.*

object CreatePersonTest {
@JvmStatic
fun main(args: Array<String>) {
    createPerson()
}

fun createPerson() {
    // 1.创建默认的ClassPool,ClassPool是一个存储CtClass的Hash表
    val pool: ClassPool = ClassPool.getDefault()

    // 2.新建一个空类,叫Person类
    val person: CtClass = pool.makeClass("com.test.Person")

    // 3.新增一个字段 private String name;
    // 字段名为name
    val nameField: CtField = CtField(pool.get("java.lang.String"), "name", person)
    // 设置字段的访问类型为private
    nameField.modifiers = Modifier.PRIVATE

    // 新增age字段,也可以使用make方法快速创建
    val ageField = CtField.make("private int age;", person)

    // 给字段进行默认初始化,并添加到Person类中
    person.addField(nameField, CtField.Initializer.constant("小明"))
    person.addField(ageField, CtField.Initializer.constant(20))

    // 3.生成getter、setter方法
    person.addMethod(CtNewMethod.setter("setName", nameField))
    person.addMethod(CtNewMethod.getter("getName", nameField))
    person.addMethod(CtNewMethod.setter("setAge",ageField))
    person.addMethod(CtNewMethod.getter("getAge",ageField))

    // 4.添加无参数的构造方法
    val cons: CtConstructor = CtConstructor(arrayOf<CtClass>(), person)
    // 设置构造方法的方法体
    cons.setBody("{name=\"老王\";age=30;}")
    // 添加到person类中
    person.addConstructor(cons)

    // 5.添加有参的构造函数
    val cons2: CtConstructor = CtConstructor(arrayOf(pool.get("java.lang.String"), CtClass.intType), person)
    // $0=this, $1,$2,$3...代表第几个参数
    cons2.setBody("{$0.name=$1;$0.age=$2;}")
    // 添加到person类中
    person.addConstructor(cons2)

    // 6.创建一个名为printName的方法,无参数,无返回值,输出name值
    val printName: CtMethod = CtMethod(CtClass.voidType, "printName", arrayOf<CtClass>(), person)
    // 设置方法访问类型
    printName.modifiers = Modifier.PUBLIC
    printName.setBody("{System.out.println(name);}")

    // 上面的方式可以换成这种快速创建
    val printAge: CtMethod = CtMethod.make("public void printAge(){System.out.println(age);}", person)

    // 添加到person类中
    person.addMethod(printName)
    person.addMethod(printAge)

    // 7.将创建的类对象编译成.class文件,输出到指定到路径为当前路径下的out/class路径
    person.writeFile("./out/class/")
}
}

跟咱们预想的一样。在 Javassist 中,类 Javaassit.CtClass 表示 class 文件。一个 GtClass (编译时类)对象可以处理一个 class 文件,ClassPool是 CtClass 对象的容器。它按需读取类文件来构造 CtClass 对象,并且保存 CtClass 对象以便以后使用。

需要注意的是 ClassPool 会在内存中维护所有被它创建过的 CtClass,当 CtClass 数量过多时,会占用大量的内存,API中给出的解决方案是 有意识的调用CtClass的detach()方法以释放内存。

1.2 ClassPool的相关方法

getDefault : 返回默认的ClassPool 是单例模式的,一般通过该方法创建我们的ClassPool;
appendClassPath, insertClassPath : 将一个ClassPath加到类搜索路径的末尾位置 或 插入到起始位置。通常通过该方法写入额外的类搜索路径,以解决多个类加载器环境中找不到类的尴尬;
toClass : 将修改后的CtClass加载至当前线程的上下文类加载器中,CtClass的toClass方法是通过调用本方法实现。需要注意的是一旦调用该方法,则无法继续修改已经被加载的class;
get , getCtClass : 根据类路径名获取该类的CtClass对象,用于后续的编辑。

1.3 CtClass的相关方法

freeze: 冻结一个类,使其不可修改;
isFrozen : 判断一个类是否已被冻结;
prune : 删除类不必要的属性,以减少内存占用。调用该方法后,许多方法无法将无法正常使用,慎用;
defrost : 解冻一个类,使其可以被修改。如果事先知道一个类会被defrost, 则禁止调用 prune 方法;
detach : 将该class从ClassPool中删除;
writeFile : 根据CtClass生成 .class 文件;
toClass : 通过类加载器加载该CtClass。
setInterfaces: 添加父接口
setSuperclass: 添加父类

1.4 CtMethod的相关方法

上面我们创建一个新的方法使用了CtMethod类。CtMthod代表类中的某个方法,可以通过CtClass提供的API获取或者CtNewMethod新建,通过CtMethod对象可以实现对方法的修改。

insertBefore : 在方法的起始位置插入代码;
insterAfter : 在方法的所有 return 语句前插入代码以确保语句能够被执行,除非遇到exception;
insertAt : 在指定的位置插入代码;
setBody: 将方法的内容设置为要写入的代码,当方法被 abstract修饰时,该修饰符被移除;
make : 创建一个新的方法。

注意到在上面代码中的:setBody()的时候我们使用了一些符号:
cons2.setBody("{$0.name=$1;$0.age=$2;}")
具体还有很多的符号可以使用,但是不同符号在不同的场景下会有不同的含义,后面会介绍,也可以看javassist 的说明文档。

1.5 调用生成的类对象

上面的案例是创建一个类对象然后输出该对象编译完之后的 .class 文件。那如果我们想调用生成的类对象中的属性或者方法应该怎么去做呢?

1.5.1 通过反射调用

通过CtClass的toClass方法可以转化成Class对象,然后就可以通过反射的操作来操作目标类的成员了。

fun callByReflect() {
// 1.创建classPool
val pool: ClassPool = ClassPool.getDefault()
// 2.添加目标类的搜索路径
pool.insertClassPath("./out/class/")
// 3.获取Person.class的CtClass对象
val person: CtClass = pool.get("com.test.Person")
// 4.创建Person对象
// 先通过CtClass的toClass让类加载器加载该CtClass,然后使用反射创建对象
val personObj = person.toClass().newInstance()
// 5.反射调用setter方法
val setName = personObj.javaClass.getMethod("setName", String::class.java)
setName.invoke(personObj, "老王")
// 6.反射执行printName方法
val printName = personObj.javaClass.getMethod("printName")
printName.invoke(personObj)
}

1.5.2 通过接口调用

上面两种其实都是通过反射的方式去调用,问题在于我们的工程中其实并没有这个类对象,所以反射的方式比较麻烦,并且开销也很大。那么如果你的类对象可以抽象为一些方法得合集,就可以考虑为该类生成一个接口类。这样在newInstance()的时候我们就可以强转为接口,可以将反射的那一套省略掉了。

还拿上面的Person类来说,新建一个IPerson接口类:

package com.demo

/**
* @author  chenyousheng
* @date  2022/2/12
* @desc Person类接口
*/
interface IPerson {
fun setName(name: String)
fun getName(): String
fun printName()
}

通过CtClass的setInterfaces方法可以给目标类添加一个父接口,也就是上面我们定义的接口类。

fun callByInterface() {
// 1.创建classPool
val pool: ClassPool = ClassPool.getDefault()
// 2.添加类搜索路径,这里是IPerson接口class文件所在的路径,
// 如果IPerson源码文件的根文件是src目录,那么也可以不需要指定class文件的目录,因为ClassPool默认搜索路径能够找到
// pool.appendClassPath("./build/classes/kotlin/main/com/demo/")
// 这里是Person类的class所在的路径,由于这个路径不是默认搜索路径,所以需要指定
pool.appendClassPath("./out/class")
// 3.获取接口类
val personInter: CtClass = pool.get("com.demo.IPerson")
// 4.获取Person类
val person: CtClass = pool.get("com.test.Person")
// 5.让Person类实现IPerson接口
person.interfaces = arrayOf<CtClass>(personInter)
// 6.下面创建Person对象,就可以强转成接口类了
val personObj: IPerson = person.toClass().newInstance() as IPerson
// 然后就可以愉快的调用方法了,不需要反射来操作了
println(personObj.getName())
personObj.setName("老王2")
personObj.printName()

}

1.6 修改现有的类对象

前面说到新增一个类对象。这个使用场景目前还没有遇到过,一般会遇到的使用场景应该是修改已有的类。比如常见的日志切面,权限切面。我们利用javassist来实现这个功能。

有如下类对象:

package com.demo;

public class PersonService {

public void fly() {
    System.out.println("我飞起来了");
}
}

下面的例子是演示如何来操作PersonService.class文件

package com.demo

import javassist.ClassPool
import javassist.CtClass
import javassist.CtMethod
import javassist.Modifier

/**
* @author  chenyousheng
* @date  2022/2/12
* @desc 修改已存在的class
*/
object UpdatePersonTest {
@JvmStatic
fun main(args: Array<String>) {
    update()
}

fun update() {
    // 1.创建classPool,使用的是默认系统的类搜索路径。
    val pool: ClassPool = ClassPool.getDefault()

    // 2.获取要修改的class
    val cc: CtClass = pool.get("com.demo.PersonService")

    // 3.获取目标类的方法
    val fly: CtMethod = cc.getDeclaredMethod("fly")
    // 在方法执行前插入代码
    fly.insertBefore("System.out.println(\"起飞前准备降落伞\");")
    // 在方法执行后插入代码
    fly.insertAfter("System.out.println(\"成功落地\");")

    // 4.新增一个方法
    val newMethod: CtMethod = CtMethod(
        CtClass.voidType, "joinFriend",
        arrayOf<CtClass>(), cc
    )
    // 设置方法访问权限
    newMethod.modifiers = Modifier.PUBLIC
    // 设置方法的方法体
    newMethod.setBody("{System.out.println(\"加个好友吧\");}")
    // 加到目标类中
    cc.addMethod(newMethod)

    // 5.实例化目标类
    val personService = cc.toClass().newInstance()
    // 调用fly方法
    personService.javaClass.getMethod("fly").invoke(personService)
    // 调用joinFriend方法
    personService.javaClass.getMethod("joinFriend").invoke(personService)

}
}

运行结果如下:

起飞前准备降落伞
我飞起来了
成功落地
加个好友吧

另外需要注意的是:上面的insertBefore() 和 setBody()中的语句,如果你是单行语句可以直接用双引号,但是有多行语句的情况下,你需要将多行语句用{}括起来。javassist只接受单个语句或用大括号括起来的语句块。

二、将类冻结

如果一个 CtClass 对象通过 writeFile(), toClass(), toBytecode() 被转换成一个类文件,此 CtClass 对象会被冻结起来,不允许再修改。因为一个类只能被 JVM 加载一次。

但是,一个冷冻的 CtClass 也可以被解冻,例如:

CtClasss cc = ...;
:
cc.writeFile();
cc.defrost();
cc.setSuperclass(...);    // 因为类已经被解冻,所以这里可以调用成功

调用 defrost() 之后,此 CtClass 对象又可以被修改了。

如果 ClassPool.doPruning 被设置为 true,Javassist 在冻结 CtClass 时,会修剪 CtClass 的数据结构。为了减少内存的消耗,修剪操作会丢弃 CtClass 对象中不必要的属性。例如,Code_attribute 结构会被丢弃。一个 CtClass 对象被修改之后,方法的字节码是不可访问的,但是方法名称、方法签名、注解信息可以被访问。修剪过的 CtClass 对象不能再次被解冻。ClassPool.doPruning 的默认值为 false。

stopPruning() 可以用来驳回修剪操作。

CtClasss cc = ...;
cc.stopPruning(true);
:
cc.writeFile(); // 转换成一个 class 文件
// cc is not pruned.

这个 CtClass 没有被修剪,所以在 writeFile() 之后,可以被解冻。

三、类搜索路径

通过ClassPool.getDefault()获取的ClassPool使用 JVM 的类搜索路径。如果程序运行在JBoss或者Tomcat等 Web 服务器上,ClassPool可能无法找到用户的类,因为 Web 服务器使用多个类加载器作为系统类加载器。在这种情况下,ClassPool 必须添加额外的类搜索路径。

1)通过ClassClassPath添加搜索路径

pool.insertClassPath(new ClassClassPath(Person.getClass()));

上面的语句将Person类添加到pool的类加载路径中。但在实践中,我发现通过这个可以将Person类所在的整个jar包添加到类加载路径中。

2)通过指定目录来添加搜索路径
也可以注册一个目录作为类搜索路径:
pool.insertClassPath("/usr/javalib");则是将 /usr/javalib目录添加到类搜索路径中。

3)通过URL指定搜索路径

ClassPool pool = ClassPool.getDefault();
ClassPath cp = new URLClassPath("www.sample.com", 80, "/out/", "com.test.");
pool.insertClassPath(cp);

上述代码将http://www.sample.com:80/out添加到类搜索路径。并且这个URL只能搜索 com.test包里面的类。例如,为了加载 com.test.Person,它的类文件会从获取http://www.sample.com:80/out/com/test/Person.class获取。

4)通过ByteArrayPath添加搜索路径

ClassPool cp = ClassPool.getDefault();
byte[] buf = 字节数组;
String name = 类名;
cp.insertClassPath(new ByteArrayClassPath(name, buf));
CtClass cc = cp.get(name);

示例中的 CtClass 对象是字节数据buf代表的class文件。将对应的类名传递给ClassPool的get()方法,就可以从字节数组中读取到对应的类文件。

5)通过输入流加载class
如果你不知道类的全名,可以使用makeClass()方法:

ClassPool cp = ClassPool.getDefault();
InputStream ins =  class文件对应的输入流;
CtClass cc = cp.makeClass(ins);

makeClass()返回从给定输入流构造的CtClass对象。你可以使用makeClass()将类文件提供给ClassPool对象。如果搜索路径包含大的jar文件,这可能会提高性能。由于ClassPool对象按需读取类文件,它可能会重复搜索整个jar文件中的每个类文件。makeClass()可以用于优化此搜索。由makeClass()构造的CtClass保存在ClassPool对象中,从而使得类文件不会再被读取。

四、$开头的特殊字符

符号含义
$0, $1, $2, …$0=this,$1表示方法的第一个参数,依次类推,如果方法是静态的,则 $0 不可用
$args方法参数数组.它的类型为 Object[],$args[0]=1 , 1,1,args[1]=$2
$r返回结果的类型,用于强制类型转换
$w包装器类型,用于强制类型转换,当放回值是包装类型时,可以用此来强转
$_返回值,一般在insertAfter中用到,用于得到原方法的返回值
$sig参数类型数组,$sig[0]表示第一个参数类型
$type返回值类型,一般在insertAfter中用到,即$_的类型
$class$0或this的类型
$e异常类型

1)$e的使用场景
在给方法添加catch语句块的时候就会用到了,例如

// 创建printAge方法
val printAge: CtMethod = CtMethod.make("public void printAge(){System.out.println(age);}", person)
// 给printAge添加catch语句块,在kotlin中$需要转义
printAge.addCatch("{System.out.println(\$e);throw \$e;}", pool.get("java.lang.Exception"))
// 添加到person类中
person.addMethod(printAge)

效果如下:

public void printAge() {
try {
    System.out.println(this.age);
} catch (Exception var2) {
    System.out.println(var2);
    throw var2;
}
}

2)$r的使用场景
例如给Person类添加一个convert方法

val convert = CtNewMethod.make("public int convert(){ Double d= 12.5;return (\$r)d;} ", person)
person.addMethod(convert)

效果如下:

public int convert() {
double var1 = 12.5D;
return (Integer)var1;
}

五、ProxyFactory的使用

通过ProxyFactory可以实现动态代理的方式在处理目标类的方法。假设要被代理的类定义如下:

package com.demo;

public class PersonService {

public int fly() {
    System.out.println("我飞起来了");
    return 0;
}
}

通过ProxyFactory动态代理PersonService的fly方法,具体如下:

package com.demo

import javassist.ClassPool
import javassist.util.proxy.MethodHandler
import javassist.util.proxy.ProxyFactory
import javassist.util.proxy.ProxyObject


/**
* @author  chenyousheng
* @date  2022/2/13
* @desc ProxyFactory动态代理目标类方法
*/
object ProxyFactoryTest {
@JvmStatic
fun main(args: Array<String>) {

    // 获取ClassPool
    val pool = ClassPool.getDefault()
    // 获取目标类
    val cc = pool.get("com.demo.PersonService")
    // 实例化代理类工厂
    val factory = ProxyFactory()
    // 设置代理类的父类,ProxyFactory将会动态生成一个类,继承该父类
    factory.superclass = cc.toClass()
    // 设置过滤器,判断哪些方法调用需要被拦截
    factory.setFilter {
        return@setFilter it.name == "fly"
    }
    // 创建代理类型
    val proxy = factory.createClass()
    // 创建代理实例,强转成父类
    val personService: PersonService = proxy.newInstance() as PersonService
    // 设置代理处理方法
    (personService as ProxyObject).handler = MethodHandler { self, thisMethod, proceed, args ->
        //thisMethod为被代理方法 proceed为代理方法 self为代理实例 args为方法参数
        println(thisMethod.name + "被调用前输出")
        try {
            val ret = proceed.invoke(self, *args)
            println(thisMethod.name + "正在调用,返回值: " + ret)
            return@MethodHandler ret
        } finally {
            println(thisMethod.name + "被调用后输出")
        }
    }

    // 调用代理类的fly方法
    personService.fly()
}
}

输出结果如下:

fly被调用前输出
我飞起来了
fly正在调用,返回值: 0
fly被调用后输出

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

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

相关文章

Linux I/O复用函数的使用情况和select接口的介绍

I/O 复用使得程序能同时监听多个文件描述符&#xff0c;这对于提高程序的性能至关重要。通常&#xff0c; 网络程序在下列情况下需要使用 I/O 复用技术&#xff1a; 1.TCP服务器同时要处理监听套接字和连接套接字 2.服务器同时要处理TCP请求和UDP请求。 3.程序同时要处理多个套…

直播预告 | 时序数据处理的云端利器:TDengine Cloud 详解与演示

当下&#xff0c;我们正处在一个万物互联的时代&#xff0c;大数据、云原生、AI、5G 等数字技术极大地方便了人们的生活&#xff0c;但智能物联网产生的海量数据却成为众多企业在数据处理上的巨大痛点。从本质来看&#xff0c;这些数据大多是产生自各种设备和传感器的时序数据&…

Spring种存取Bean的5种注解

存取Bean的五种注解 存储Bean对象两种方式1.添加一行bean2.使用注解的方式(5大注解)Controller(控制器存储)Service(服务存储)Repository(仓库存储)Component(组件存储)Configuration(配置存储)方法注解 Bean 获取Bean对象(三种)1.属性注入2.setter注入3.构造方法注入三种注入的…

springboot-分页功能

1.分页功能的作用 分页功能作为各类网站和系统不可或缺的部分&#xff08;例如百度搜索结果的分页等&#xff09; &#xff0c;当一个页面数据量大的时候分页作用就体现出来的&#xff0c;其作用有以下5个。 &#xff08;1&#xff09;减少系统资源的消耗 &#xff08;2&#…

Vue 3组件传值 、组件通信

本文采用<script setup />的写法&#xff0c;比options API更自由。那么我们就来说说以下七种组件通信方式&#xff1a; props emit v-model refs provide/inject eventBus vuex/pinia 举个例子 本文将使用下面的演示&#xff0c;如下图所示&#xff1a; 上图中…

mybatis粗心使用导致内存溢出

现象 服务响应变慢&#xff0c;线程日志也出现Java heap space内存溢出的错误&#xff0c;这个服务属于基础业务服务&#xff0c;出现问题要尽快的排查 分析 因为设置了gc日志和jmap启动相关参数 所以我们进行分析&#xff0c;这里模拟线上环境将堆大小参数调整到了128m&am…

【Linux】权限管理

文章目录 &#x1f4d6; 前言1. 什么是权限2. 权限管理2.1 Linux的用户分类&#xff1a;2.2 Liunx文件的分类&#xff1a;2.3 文件的访问权限2.4 文件访问权限的相关设置方法&#xff1a;chmod对文件权限的修改chown / chgrp 2.5 以八进制修改文件权限&#xff1a;2.6 默认权限…

Springsecurity课程笔记06-13章基于数据库的方法授权

动力节点Springsecurity视频课程 6 密码处理 6.1 为什么要加密&#xff1f; csdn 密码泄露事件 泄露事件经过&#xff1a;https://www.williamlong.info/archives/2933.html 泄露数据分析&#xff1a;https://blog.csdn.net/crazyhacking/article/details/10443849 6.2加密…

IJKPLAYER源码分析-常用API

前言 本文简要介绍IJKPLAYER的几个常用API&#xff0c;以API使用的角度&#xff0c;来审视其内部运作原理。这里以iOS端直播API调用切入。 调用流程 init 创建播放器实例后&#xff0c;会先调用init方法进行初始化&#xff1a; - (IJKFFMediaPlayer *)init {self [super ini…

计算机网络复习题+答案

文章目录 导文题目一、单项选择题二、填空题三、判断改错题,判断下列命题正误,正确的在其题干后的括号内打“√”,错误的打“”,并改正。四、名词解释五、简答题六、应用题导文 计算机网络复习题 题目 一、单项选择题 在应用层协议中,主要用于IP地址自动配置的协议是: (…

文案自动修改软件-文案自动改写的免费软件下载

文章生成器ai写作机器人 随着人工智能技术的飞速发展&#xff0c;越来越多的新型产品被推向市场。其中&#xff0c;文章生成器AI写作机器人是一个备受关注的新兴行业。它使用机器学习和自然语言处理等技术&#xff0c;为用户自动生成高质量的文章和内容&#xff0c;帮助用户在…

Python——第2章 数据类型、运算符与内置函数

目录 1 赋值语句 2 数据类型 2.1 常用内置数据类型 2.1.1 整数、实数、复数 2.1.2 列表、元组、字典、集合 2.1.3 字符串 2.2 运算符与表达式 2.2.1 算术运算符 2.2.2 关系运算符 2.2.3 成员测试运算符 2.2.4 集合运算符 2.2.5 逻辑运算符 2.3 常用内置…

本地搭建属于自己的ChatGPT:基于PyTorch+ChatGLM-6b+Streamlit+QDrant+DuckDuckGo

本地部署chatglm及缓解时效性问题的思路&#xff1a; 模型使用chatglm-6b 4bit&#xff0c;推理使用hugging face&#xff0c;前端应用使用streamlit或者gradio。 微调对显存要求较高&#xff0c;还没试验。可以结合LoRA进行微调。 缓解时效性问题&#xff1a;通过本地数据库…

Mybatis高级映射及延迟加载

准备数据库表&#xff1a;一个班级对应多个学生。班级表&#xff1a;t_clazz&#xff1b;学生表&#xff1a;t_student 创建pojo&#xff1a;Student、Clazz // Student public class Student {private Integer sid;private String sname;//...... }// Clazz public class Cla…

Flutter PC桌面端 控制应用尺寸是否允许放大缩小

一、需求 桌面端中&#xff0c;登录、注册、找回密码页面不允许用户手动放大缩小&#xff0c;主页面允许 二、插件 window_manager 使用教程请参照这篇博客&#xff1a;Flutter桌面端开发——window_manager插件的使用 题外话&#xff1a; 之前使用的是bitsdojo_window插件…

[golang gin框架] 25.Gin 商城项目-配置清除缓存以及前台列表页面数据渲染公共数据

配置清除缓存 当进入前台首页时,会缓存对应的商品相关数据,这时,如果后台修改了商品的相关数据,缓存中的对应数据并没有随之发生改变,这时就需要需改对应的缓存数据,这里有两种方法: 方法一 在管理后台操作直接清除缓存中的所有数据,当再次访问前台首页时,就会先从数据库中获取…

记frp内网穿透配置

这两天由于想给客户看一下我们的系统&#xff0c;于是想到用内网穿透&#xff0c;但是怎么办呢&#xff0c;没有用过呀&#xff0c;于是各处找资料&#xff0c;但是搞完以后已经不记得参考了那些文档了&#xff0c;对不起各位大神&#xff0c;就只能写出过程和要被自己蠢死的错…

初识C++(二)

在初识c&#xff08;一&#xff09;当中我们已经向大家介绍了四个c和C语言不同的使用方法。接下来我们再来向大家介绍另外的一些新的c语言的使用方法。 &#x1f335;引用 简单一点来说引用就是给已存在的变量起一个别名。这个别名通常的作用和C语言当中的指针类似。我们可以通…

牛客网刷题总结

1.利用%符号获取特定位数的数字。 2.强制类型转换 &#xff08;将float转换为int &#xff09; 3.计算有关浮点型数据时&#xff0c;要注意你计算过程中所有的数据都是浮点型 4.0/3.0 ! 4/3 4.通过位操作符实现输出2的倍数&#xff08;对于位操作符不熟悉的小伙伴可以看看我…

基于Java+SpringBoot+vue实现图书借阅和销售商城一体化系统

基于JavaSpringBootvue实现图书借阅和销售商城一体化系统 博主介绍&#xff1a;5年java开发经验&#xff0c;专注Java开发、定制、远程、指导等,csdn特邀作者、专注于Java技术领域 作者主页 超级帅帅吴 Java项目精品实战案例《500套》 欢迎点赞 收藏 ⭐留言 文末获取源码联系方…