Java中的SpringAOP、代理模式、常用AspectJ注解详解

news/2024/11/17 23:56:22/文章来源:https://www.cnblogs.com/erichi101/p/18293601

 

 
这篇文章主要介绍了Java中的SpringAOP、代理模式、常用AspectJ注解详解,Spring提供了面向切面编程的丰富支持,允许通过分离应用的业务逻辑与系统级服务,例如审计和事务管理进行内聚性的开发,需要的朋友可以参考下
 

Java技术迷

一、AOP简述

回到主题,何为AOP?AOP即面向切面编程——Spring提供了面向切面编程的丰富支持,允许通过分离应用的业务逻辑与系统级服务(例如审计(auditing)和事务(transaction)管理)进行内聚性的开发。

应用对象只实现它们应该做的——完成业务逻辑——仅此而已。

它们并不负责(甚至是意识)其它的系统级关注点,例如日志或事务支持。

如下图,可以很直接明了的展示整个AOP的过程:

 

 

1.1 一些基本概念

通知(Adivce)

通知有5种类型:

我们可能会问,那通知对应系统中的代码是一个方法、对象、类、还是接口什么的呢?

我想说一点,其实都不是,你可以理解通知就是对应我们日常生活中所说的通知,比如‘某某人,你2019年9月1号来学校报个到’,通知更多地体现一种告诉我们(告诉系统何)何时执行,规定一个时间,在系统运行中的某个时间点(比如抛异常啦!方法执行前啦!), 并非对应代码中的方法!并非对应代码中的方法!并非对应代码中的方法!

  • Before 在方法被调用之前调用
  • After 在方法完成后调用通知,无论方法是否执行成功
  • After-returning 在方法成功执行之后调用通知
  • After-throwing 在方法抛出异常后调用通知
  • Around 通知了好、包含了被通知的方法,在被通知的方法调用之前后调用之后执行自定义的行为
  • 切点(Pointcut)
    • 切点在Spring AOP中确实是对应系统中的方法。但是这个方法是定义在切面中的方法,一般和通知一起使用,一起组成了切面。
  • 连接点(Join point)
    • 比如:方法调用、方法执行、字段设置/获取、异常处理执行、类初始化、甚至是 for 循环中的某个点 理论上, 程序执行过程中的任何时点都可以作为作为织入点, 而所有这些执行时点都是 Joint point 但 Spring AOP 目前仅支持方法执行 (method execution) 也可以这样理解,连接点就是你准备在系统中执行切点和切入通知的地方(一般是一个方法,一个字段)
  • 切面(Aspect)
    • 切面是切点和通知的集合,一般单独作为一个类。通知和切点共同定义了关于切面的全部内容,它是什么时候,在何时和何处完成功能。
  • 引入(Introduction)
    • 引用允许我们向现有的类添加新的方法或者属性
  • 织入(Weaving)
    • 组装方面来创建一个被通知对象。这可以在编译时完成(例如使用AspectJ编译器),也可以在运行时完成。Spring和其他纯Java AOP框架一样,在运行时完成织入。

二、代理模式

首先AOP思想的实现一般都是基于代理模式,在JAVA中一般采用JDK动态代理模式,但是我们都知道,JDK动态代理模式只能代理接口,如果要代理类那么就不行了。

因此,Spring AOP 会这样子来进行切换,因为Spring AOP 同时支持 CGLIB、ASPECTJ、JDK动态代理,当你的真实对象有实现接口时,Spring AOP会默认采用JDK动态代理,否则采用cglib代理。

  • 如果目标对象的实现类实现了接口,Spring AOP 将会采用 JDK 动态代理来生成 AOP 代理类;
  • 如果目标对象的实现类没有实现接口,Spring AOP 将会采用 CGLIB 来生成 AOP 代理类——不过这个选择过程对开发者完全透明、开发者也无需关心。

这里简单说说代理模式,代理模式的UML类图如下:

2.1 静态代理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
//接口类:
interface Person {
    void speak();
}
//真实实体类:
class Actor implements Person {
    private String content;
    public Actor(String content) {
        this.content = content;
    }
    @Override
    public void speak() {
        System.out.println(this.content);
    }
}
//代理类:
class Agent implements Person {
    private Actor actor;
    private String before;
    private String after;
    public Agent(Actor actor, String before, String after) {
        this.actor = actor;
        this.before = before;
        this.after = after;
    }
    @Override
    public void speak() {
        //before speak
        System.out.println("Before actor speak, Agent say: " + before);
        //real speak
        this.actor.speak();
        //after speak
        System.out.println("After actor speak, Agent say: " + after);
    }
}
//测试方法:
public class StaticProxy {
    public static void main(String[] args) {
        Actor actor = new Actor("I am a famous actor!");
        Agent agent = new Agent(actor, "Hello I am an agent.", "That's all!");
        agent.speak();
    }
}

2.2 动态代理

在讲JDK的动态代理方法之前,不妨先想想如果让你来实现一个可以任意类的任意方法的代理类,该怎么实现?有个很naive的做法,通过反射获得Class和Method,再调用该方法,并且实现一些代理的方法。我尝试了一下,很快就发现问题所在了。于是乎,还是使用JDK的动态代理接口吧。

JDK自带方法

首先介绍一下最核心的一个接口和一个方法:

首先是java.lang.reflect包里的InvocationHandler接口:

1
2
3
4
public interface InvocationHandler {
    public Object invoke(Object proxy, Method method, Object[] args)
        throws Throwable;
}

我们对于被代理的类的操作都会由该接口中的invoke方法实现,其中的参数的含义分别是:

  • proxy:被代理的类的实例
  • method:调用被代理的类的方法
  • args:该方法需要的参数

使用方法首先是需要实现该接口,并且我们可以在invoke方法中调用被代理类的方法并获得返回值,自然也可以在调用该方法的前后去做一些额外的事情,从而实现动态代理,下面的例子会详细写到。

另外一个很重要的静态方法是java.lang.reflect包中的Proxy类的newProxyInstance方法:

1
2
3
4
public static Object newProxyInstance(ClassLoader loader,
                                         Class<?>[] interfaces,
                                         InvocationHandler h)
       throws IllegalArgumentException

其中的参数含义如下:

  • loader:被代理的类的类加载器
  • interfaces:被代理类的接口数组
  • invocationHandler:就是刚刚介绍的调用处理器类的对象实例

该方法会返回一个被修改过的类的实例,从而可以自由的调用该实例的方法。下面是一个实际例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
Fruit接口:
    public interface Fruit {
        public void show();
    }
Apple实现Fruit接口:
    public class Apple implements Fruit{
        @Override
        public void show() {
            System.out.println("<<<);
        }
    }
代理类Agent.java:
    public class DynamicAgent {
        //实现InvocationHandler接口,并且可以初始化被代理类的对象
        static class MyHandler implements InvocationHandler {
            private Object proxy;
            public MyHandler(Object proxy) {
                this.proxy = proxy;
            }
            //自定义invoke方法
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                System.out.println(">>>>before invoking");
                //真正调用方法的地方
                Object ret = method.invoke(this.proxy, args);
                System.out.println(">>>>after invoking");
                return ret;
            }
        }
        //返回一个被修改过的对象
        public static Object agent(Class interfaceClazz, Object proxy) {
            return Proxy.newProxyInstance(interfaceClazz.getClassLoader(), new Class[]{interfaceClazz},
                    new MyHandler(proxy));
        }   
    }
测试类:
    public class ReflectTest {
        public static void main(String[] args) throws InvocationTargetException, IllegalAccessException {
            //注意一定要返回接口,不能返回实现类否则会报错
            Fruit fruit = (Fruit) DynamicAgent.agent(Fruit.class, new Apple());
            fruit.show();
        }
    }

结果:

可以看到对于不同的实现类来说,可以用同一个动态代理类来进行代理,实现了“一次编写到处代理”的效果。

但是这种方法有个缺点,就是被代理的类一定要是实现了某个接口的,这很大程度限制了本方法的使用场景。下面还有另外一个使用了CGlib增强库的方法。

2.3 CGLIB库的方法

CGlib是一个字节码增强库,为AOP等提供了底层支持。下面看看它是怎么实现动态代理的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
public class CGlibAgent implements MethodInterceptor {
    private Object proxy;
    public Object getInstance(Object proxy) {
        this.proxy = proxy;
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(this.proxy.getClass());
        // 回调方法
        enhancer.setCallback(this);
        // 创建代理对象
        return enhancer.create();
    }
    //回调方法
    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        System.out.println(">>>>before invoking");
        //真正调用
        Object ret = methodProxy.invokeSuper(o, objects);
        System.out.println(">>>>after invoking");
        return ret;
    }
    public static void main(String[] args) {
        CGlibAgent cGlibAgent = new CGlibAgent();
        Apple apple = (Apple) cGlibAgent.getInstance(new Apple());
        apple.show();
    }
}

三、Spring中的AOP: @AspectJ

3.1 @AspectJ 由来

AspectJ是一个AOP框架,它能够对java代码进行AOP编译(一般在编译期进行),让java代码具有AspectJ的AOP功能(当然需要特殊的编译器),可以这样说AspectJ是目前实现AOP框架中最成熟,功能最丰富的语言,更幸运的是,AspectJ与java程序完全兼容,几乎是无缝关联,因此对于有java编程基础的工程师,上手和使用都非常容易。

其实AspectJ单独就是一门语言,它需要专门的编译器(ajc编译器). Spring AOP 与ApectJ的目的一致,都是为了统一处理横切业务,但与AspectJ不同的是,Spring AOP并不尝试提供完整的AOP功能(即使它完全可以实现),Spring AOP 更注重的是与Spring IOC容器的结合,并结合该优势来解决横切业务的问题,因此在AOP的功能完善方面,相对来说AspectJ具有更大的优势,同时,Spring注意到AspectJ在AOP的实现方式上依赖于特殊编译器(ajc编译器),因此Spring很机智回避了这点,转向采用动态代理技术的实现原理来构建Spring AOP的内部机制(动态织入),这是与AspectJ(静态织入)最根本的区别。在AspectJ 1.5后,引入@Aspect形式的注解风格的开发,Spring也非常快地跟进了这种方式,因此Spring 2.0后便使用了与AspectJ一样的注解。请注意,Spring 只是使用了与 AspectJ 5 一样的注解,但仍然没有使用 AspectJ 的编译器,底层依是动态代理技术的实现,因此并不依赖于 AspectJ 的编译器。

所以,Spring AOP虽然是使用了AspectJ那一套注解,其实实现AOP的底层是使用了动态代理(JDK或者CGLib)来动态植入。

3.2 举个栗子

小狗类,会说话:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class Dog {
    private String name;
    public void say(){
        System.out.println(name + "在汪汪叫!...");
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
}
切面类:
@Aspect //声明自己是一个切面类
public class MyAspect {
    /**
     * 前置通知
     */
     //@Before是增强中的方位
     // @Before括号中的就是切入点了
     //before()就是传说的增强(建言):说白了,就是要干啥事.
    @Before("execution(* com.zdy..*(..))")
    public void before(){
        System.out.println("前置通知....");
    }
}

这个类是重点,先用@Aspect声明自己是切面类,然后before()为增强,@Before(方位)+切入点可以具体定位到具体某个类的某个方法的方位. Spring配置文件:

1
2
3
4
5
6
7
8
9
//开启AspectJ功能.
    <aop:aspectj-autoproxy />
    <bean id="dog" class="com.zdy.Dog" />
    <bean name="myAspect" class="com.zdy.MyAspect"/>
然后Main方法:
        ApplicationContext ac =new ClassPathXmlApplicationContext("applicationContext.xml");
        Dog dog =(Dog) ac.getBean("dog");
        System.out.println(dog.getClass());
        dog.say();

输出结果:

class com.zdy.Dog$$EnhancerBySpringCGLIB$$80a9ee5f
前置通知....
null在汪汪叫!...

说白了,就是把切面类丢到容器,开启一个AdpectJ的功能,Spring AOP就会根据切面类中的(@Before+切入点)定位好具体的类的某个方法(我这里定义的是com.zdy包下的所有类的所有方法),然后把增强before()切入进去.

3.3 举个Spring Boot中的栗子

这个栗子很实用,关于Aop做切面去统一处理Web请求的日志:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Aspect
@Component
public class WebLogAspect {
    private Logger logger = Logger.getLogger(getClass());
    @Pointcut("execution(public * com.didispace.web..*.*(..))")
    public void webLog(){}
    @Before("webLog()")
    public void doBefore(JoinPoint joinPoint) throws Throwable {
        // 接收到请求,记录请求内容
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        // 记录下请求内容
        logger.info("URL : " + request.getRequestURL().toString());
        logger.info("HTTP_METHOD : " + request.getMethod());
        logger.info("IP : " + request.getRemoteAddr());
        logger.info("CLASS_METHOD : " + joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName());
        logger.info("ARGS : " + Arrays.toString(joinPoint.getArgs()));
    }
    @AfterReturning(returning = "ret", pointcut = "webLog()")
    public void doAfterReturning(Object ret) throws Throwable {
        // 处理完请求,返回内容
        logger.info("RESPONSE : " + ret);
    }
}

可以看上面的例子,通过 @Pointcut 定义的切入点为 com.didispace.web 包下的所有函数(对web层所有请求处理做切入点),然后通过 @Before 实现,对请求内容的日志记录(本文只是说明过程,可以根据需要调整内容),最后通过 @AfterReturning 记录请求返回的对象。

通过运行程序并访问: //localhost:8080/hello?name=didi ,可以获得下面的日志输出

2016-05-19 13:42:13,156  INFO WebLogAspect:41 - URL : http://localhost:8080/hello
2016-05-19 13:42:13,156  INFO WebLogAspect:42 - HTTP_METHOD : http://localhost:8080/hello
2016-05-19 13:42:13,157  INFO WebLogAspect:43 - IP : 0:0:0:0:0:0:0:1
2016-05-19 13:42:13,160  INFO WebLogAspect:44 - CLASS_METHOD : com.didispace.web.HelloController.hello
2016-05-19 13:42:13,160  INFO WebLogAspect:45 - ARGS : [didi]
2016-05-19 13:42:13,170  INFO WebLogAspect:52 - RESPONSE:Hello didi

3.4 Spring AOP支持的几种AspectJ注解

  • 前置通知@Before: 前置通知通过@Before注解进行标注,并可直接传入切点表达式的值,该通知在目标函数执行前执行,注意JoinPoint,是Spring提供的静态变量,通过joinPoint 参数,可以获取目标对象的信息,如类名称,方法参数,方法名称等,该参数是可选的。
1
2
3
4
@Before("execution(...)")
public void before(JoinPoint joinPoint){
    System.out.println("...");
}
  • 后置通知@AfterReturning: 通过@AfterReturning注解进行标注,该函数在目标函数执行完成后执行,并可以获取到目标函数最终的返回值returnVal,当目标函数没有返回值时,returnVal将返回null,必须通过returning = “returnVal”注明参数的名称而且必须与通知函数的参数名称相同。请注意,在任何通知中这些参数都是可选的,需要使用时直接填写即可,不需要使用时,可以完成不用声明出来。
1
2
3
4
@AfterReturning(value="execution(...)",returning = "returnVal")
public void AfterReturning(JoinPoint joinPoint,Object returnVal){
   System.out.println("我是后置通知...returnVal+"+returnVal);
}
  • 异常通知 @AfterThrowing:该通知只有在异常时才会被触发,并由throwing来声明一个接收异常信息的变量,同样异常通知也用于Joinpoint参数,需要时加上即可.
1
2
3
4
@AfterThrowing(value="execution(....)",throwing = "e")
public void afterThrowable(Throwable e){
  System.out.println("出现异常:msg="+e.getMessage());
}
  • 最终通知 @After:该通知有点类似于finally代码块,只要应用了无论什么情况下都会执行.
1
2
3
4
@After("execution(...)")
public void after(JoinPoint joinPoint) {
    System.out.println("最终通知....");
}
  • 环绕通知 @Around: 环绕通知既可以在目标方法前执行也可在目标方法之后执行,更重要的是环绕通知可以控制目标方法是否指向执行,但即使如此,我们应该尽量以最简单的方式满足需求,在仅需在目标方法前执行时,应该采用前置通知而非环绕通知。案例代码如下第一个参数必须是ProceedingJoinPoint,通过该对象的proceed()方法来执行目标函数,proceed()的返回值就是环绕通知的返回值。同样的,ProceedingJoinPoint对象也是可以获取目标对象的信息,如类名称,方法参数,方法名称等等
1
2
3
4
5
6
7
8
@Around("execution(...)")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
    System.out.println("我是环绕通知前....");
    //执行目标函数
    Object obj= (Object) joinPoint.proceed();
    System.out.println("我是环绕通知后....");
    return obj;
}

然后说下一直用"…"忽略掉的切入点表达式,这个表达式可以不是exection(…),还有其他的一些,我就不说了,说最常用的execution:

1
2
3
4
5
6
7
8
//scope :方法作用域,如public,private,protect
//returnt-type:方法返回值类型
//fully-qualified-class-name:方法所在类的完全限定名称
//parameters 方法参数
execution(<scope> <return-type> <fully-qualified-class-name>.*(parameters))
<fully-qualified-class-name>.*(parameters)

注意这一块,如果没有精确到class-name,而是到包名就停止了,要用两个"…"来表示包下的任意类:

  • execution(* com.zdy…*(…)):com.zdy包下所有类的所有方法.
  • execution(* com.zdy.Dog.*(…)): Dog类下的所有方法.

具体详细语法,大家如果有需求自行google了,我最常用的就是这俩了。要么按照包来定位,要么按照具体类来定位.

在使用切入点时,还可以抽出来一个@Pointcut来供使用:

1
2
3
4
5
6
7
8
9
10
11
12
/**
 * 使用Pointcut定义切点
 */
@Pointcut("execution(...)")
private void myPointcut(){}
/**
 * 应用切入点函数
 */
@After(value="myPointcut()")
public void afterDemo(){
    System.out.println("最终通知....");
}

可以避免重复的execution在不同的注解里写很多遍…

3.5 AOP切面的优先级

由于通过AOP实现,程序得到了很好的解耦,但是也会带来一些问题,比如:我们可能会对Web层做多个切面,校验用户,校验头信息等等,这个时候经常会碰到切面的处理顺序问题。

所以,我们需要定义每个切面的优先级,我们需要@Order(i)注解来标识切面的优先级。i的值越小,优先级越高。假设我们还有一个切面是CheckNameAspect用来校验name必须为derry,我们为其设置@Order(10),而上文中WebLogAspect设置为@Order(5),所以WebLogAspect有更高的优先级,这个时候执行顺序是这样的:

  • 在@Before中优先执行@Order(5)的内容,再执行@Order(10)的内容
  • 在@After和@AfterReturning中优先执行@Order(10)的内容,再执行@Order(5)的内容

所以我们可以这样子总结:

  • 在切入点前的操作,按order的值由小到大执行
  • 在切入点后的操作,按order的值由大到小执行

到此这篇关于Java中的SpringAOP、代理模式、常用AspectJ注解详解的文章就介绍到这了,更多相关AOP、代理模式与AspectJ内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

zz: https://www.jb51.net/program/297684que.htm

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

浅谈qiankun微前端

qiankun是single-spa二开;使用场景:不同技术栈,不同团队,独立开发部署、增量升级;总结:解耦; 主应用: 具有整合-输入子应用的html入口;子应用 与single-spa基本一致,导出了三个生命周期函数 (bootstrap mount unmout)js沙箱: 三个沙箱(快照沙箱、支持单应用的代理沙…

Linux捣鼓记录:快速搭建alist+aria2+qbittorrent

简介:使用docker-compose创建alist aria2 qbittorrent服务,前置条件安装docker及docker-compose插件,docker镜像仓库访问不了,建议配置代理用来拉取镜像。 一、确认路径,确认UID GID,确认端口 路径 alist挂载路径: - /home/dalong/app/alist:/opt/alist/data - /home/d…

php webman使用fileboy热加载

1.下载fileboy文件下载地址:https://gitee.com/dengsgo/fileboy/releases 2.在工作目录创建一个文件夹,把下载的exr文件复制一份到文件夹,重命名为‘fileboy.exe’,添加系统变量PATH: 3.打开cmd命令窗口执行 fileboy 命令,出现以下图说明配置成功 4.切换到项目根目录,执…

统计学入门:时间序列分析基础知识详解

时间序列分析中包含了许多复杂的数学公式,它们往往难以留存于记忆之中。为了更好地掌握这些内容,本文将整理并总结时间序列分析中的一些核心概念,如自协方差、自相关和平稳性等,并通过Python实现和图形化展示这些概念,使其更加直观易懂。希望通过这篇文章帮助大家更清楚地…

组合API-ref函数

当你明确知道需要的是一个响应式数据 对象 那么就使用 reactive 即可其他情况使用ref<template><div class="container"><div>{{name}}</div><div>{{age}}</div><button @click="updateName">修改数据</butt…

重磅来袭!MoneyPrinterPlus一键发布短视频到视频号,抖音,快手,小红书上线了

一键发布短视频到视频号,抖音,快手,小红书,MoneyPrinterPlus解放你的双手。MoneyPrinterPlus开源有一段时间了,已经实现了批量短视频混剪,一键生成短视频等功能。 有些小伙伴说了,我批量生成的短视频能不能一键上传到视频号,抖音,快手,小红书这些视频平台呢?答案是必须可以…

OTA自动化测试解决方案——实车级OTA测试系统PAVELINK.OTABOX

引言往期内容里为大家介绍了OTA技术、OTA后续的发展趋势预测及OTA自动化测试解决方案。本文是OTA系列的第三篇文章,今天主要向大家介绍实车级OTA自动化测试的实现手段,并简单介绍北汇信息的实车级OTA自动化测试解决方案——PAVELINK.OTABOX。实车级OTA自动化系统目前,OTA自动…

设置DepthBufferBits和设置DepthStencilFormat的区别

1)设置DepthBufferBits和设置DepthStencilFormat的区别2)Unity打包exe后,游戏内拉不起Steam的内购3)Unity 2022以上Profiler.FlushMemoryCounters耗时要怎么关掉4)用GoodSky资产包如何实现昼夜播发不同音乐功能这是第394篇UWA技术知识分享的推送,精选了UWA社区的热门话题…

深度学习第二课 Practical Aspect of Deep learning

Practical Aspect of Deep learning week1 深度学习的实用层面 1.1 训练/开发/测试集在机器学习发展的小数据量时代,常见做法是将所有数据三七分,就是人们常说的70%验证集,30%测试集,如果没有明确设置验证集,也可以按照60%训练,20%验证和20%测试集来划分。这是前几年机器…

winform窗体DataGridView合并单元格处理

文本是使用SunnyUI的UIDataGridView控件进行演示的,同样适用于System.Windows.Forms.DataGridView控件 具体需求如下,下表是个成绩表,其中姓名、总分、平均分这三列信息重复,需要对数据表进行合并单元格处理。 实现该需求需要两个步骤: 1.给表格添加单元格重绘事件 在方法…

您的AI英语搭子!

本文由 ChatMoney团队出品 人工智能的发展,掀起了一波又一波AI浪潮,适合英语老师的AI软件也不断问世,老师们可以借助AI技术辅助自己的教学、帮助学生学习。你是否苦于想学习英语却没有语言环境,写英语内容时不知道语法和拼写是否正确,不知道表达方式是否足够的“Native”?…

Rocky Linux 9.4安装MySQL:使用RPM安装包方式

Rocky Linux 9.4安装MySQL:使用RPM安装包方式 一、安装环境安装环境如下:服务器:Rocky Linux 9.4安装版本:MySQL 8.0.38 二、安装过程和细节 1、在官网下载 RPM 安装包官网下载地址如下,这个地址里有各个版本的安装包,根据自己的版本选择,下载对应系统的安装包 https://…