Spring AOP以及统一处理

news2024/12/23 1:44:59

一.Spring AOP 

1.什么是Spring AOP 

AOP(Aspect Oriented Programming):面向切面编程,它是一种思想,它是对某一类事情的集中处理

2.AOP的作用

想象一个场景,我们在做后台系统时,除了登录和注册等几个功能不需要做用户登录验证之外,其他几乎所有页面调用的前端控制器( Controller)都需要先验证用户登录的状态,那这个时候我们要怎么处 理呢?
我们之前的处理方式是每个 Controller 都要写一遍用户登录验证,然而当你的功能越来越多,那么你要 写的登录验证也越来越多,而这些方法又是相同的,这么多的方法就会代码修改和维护的成本。那有没 有简单的处理方案呢?答案是有的,对于这种功能统一,且使用的地方较多的功能,就可以考虑 AOP 来统一处理了。

  • 除了统一的用户登录判断之外,AOP 还可以实现:
  • 统一日志记录
  • 统一方法执行时间统计
  • 统一的返回格式设置
  • 统一的异常处理
  • 事务的开启和提交等
  • AOP是OOP的补充 

 3.AOP的相关概念

1.切面(Aspect)

面(Aspect)由切点(Pointcut)和通知(Advice)组成,它既包含了横切逻辑的定义,也包

括了连接点的定义。

2.连接点 (Join Point)

应用执行过程中能够插入面的一个点,这个点可以是方法调用时,抛出异常时,甚至修改字段 时。切面代码可以利用这些点插入到应用的正常流程之中,并添加新的行为

3.切点(Pointcut)

Pointcut 是匹配 Join Point 的谓词。 Pointcut 的作用就是提供一组规则(使用 AspectJ pointcut expression language 来描述)来 匹配 Join Point,给满足规则的 Join Point 添加 Advice

4.通知(Advice)

 切面也是有目标的 ——它必须完成的工作。在 AOP 术语中,切面的工作被称之为通知。

通知:定义了切面是什么,何时使用,其描述了面要完成的工作,还解决何时执行这个工作的

问题。

Spring 切面类中,可以在方法上使用以下注解,会设置方法为通知方法,在满足条件后会通知本

方法进行调用:

  • 前置通知使用 @Before:通知方法会在目标方法调用之前执行。
  • 后置通知使用 @After:通知方法会在目标方法返回或者抛出异常后调用。
  • 返回之后通知使用 @AfterReturning:通知方法会在目标方法返回后调用。
  • 抛异常后通知使用 @AfterThrowing:通知方法会在目标方法抛出异常后调用。
  • 环绕通知使用 @Around:通知包裹了被通知的方法,在被通知的方法通知之前和调用之后执
  • 行自定义的行为。

 4.Spring AOP的实现

1.添加 AOP 框架支持

在 pom.xml 中添加如下配置:

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
<!--        Springboot test-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
<!--      Spring AOP 框架-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

    </dependencies>

2.定义切面和切点

@Component
@Slf4j
@Aspect
public class LoginAspect {
    @Pointcut("execution(* com.javastudy.springaopdemo5.controller.LoginController.*(..))")
    public void pointcut() {
    }

}

@Aspect类注解表示LoginAspect是一个切面类,@Pointcut表示定义一个切点,其中的内容表示连接点的规则,也就是包括哪些类或者方法属于这个切点,连接点.

其中 pointcut 方法为空方法,它不需要有方法体,此方法名就是起到⼀个“标识”的作用,标识下面的

通知方法具体指的是哪个切点(因为切点可能有很多个)

AspectJ 支持三种通配符

* :匹配任意字符,只匹配⼀个元素(包,类,或方法,方法参数)

.. :匹配任意字符,可以匹配多个元素 ,在表示类时,必须和 * 联合使用。

+ :表示按照类型匹配指定类的所有类,必须跟在类名后面,如 com.cad.Car+ ,表示继承该类的

所有子类包括本身

切点表达式由切点函数组成,其中 execution() 是最常用的切点函数,用来匹配方法,语法为:

execution(<修饰符><返回类型><包.类.方法(参数)><异常>)

修饰符和异常可以省略,具体含义如下:

上面我们定义的含义为,匹配com.javastudy.springaopdemo5.controller.LoginController类下所有的方法.

3.定义相关通知

先来定义controller层的内容

@RestController
@Slf4j
@RequestMapping("/user")
public class LoginController {

    @RequestMapping("/login")
    public String login() {
        log.info("login...");
        return "login...";
    }

    @RequestMapping("/register")
    public String register() {
        log.info("register...");
        return "register...";
    }

    @RequestMapping("/get")
    public String get() {
        log.info("get...");
        return "get...";
    }

}

1.前置通知 @Before

通知方法会在目标方法调用之前执行。

注意:@Before里面的内容表示切点,即在哪些接口中执行这些通知.

@Component
@Slf4j
@Aspect
public class LoginAspect {
    @Pointcut("execution(* com.javastudy.springaopdemo5.controller.LoginController.*(..))")
    public void pointcut() {
    }

    //前置通知
    @Before("pointcut()")
    public void doBefore() {
        log.info("do before....");
    }


}

 当我们访问任意一个页面的时候,控制台打印如下日志

可以看到都会先执行doBefore通知.

2.后置通知 @After

通知方法会在目标方法返回或者抛出异常后调用

    //后置通知
    @After("pointcut()")
    public void doAfter() {
        log.info("do after...");
    }

 模拟异常情况,可以在LoginController中某一个方法加10/0,观察

    @RequestMapping("/login")
    public String login() {
        log.info("login...");
        int i=10/0;
        return "login...";
    }

可以观察到在方法异常之后, @After通知仍然会执行.

3.返回之后通知 @AfterReturning

通知方法会在目标方法返回后调用

    // return 之前通知
    @AfterReturning("pointcut()")
    public void doAfterReturning() {
        log.info("do after returning...");
    }

当我们访问login接口的时候(10/0,有异常),观察是否有输出

此时是没有输出的.

访问其它接口,没有异常的接口

 此时@AfterReturning通知正常执行

4.抛异常后通知 @AfterThrowing

通知方法会在目标方法抛出异常后调用。

    //抛出异常之前通知
    @AfterThrowing("pointcut()")
    public void doAfterThrowing() {
        log.info("do after throwing");
    }

执行有异常的接口,有日志的打印

执行没有异常的接口,没有日志的打印 

5.环绕通知 @Around

通知包裹了被通知的方法,在被通知的方法通知之前和调用之后执行自定义的行为。

    @Around("pointcut()")
    public Object doAround(ProceedingJoinPoint joinPoint) {
        Object oj = null;
        log.info("环绕通知执行之前...");
        log.info(joinPoint.getSignature().toLongString());
        try {
            oj = joinPoint.proceed();//调用目标方法
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
        log.info("环绕通知执行之后....");
        return oj;
    }

执行没有异常的接口:

执行有异常的接口: 

接下来我们看看ProceedingJoinPoint 常用方法

toString连接点所在位置的相关信息
toShortString连接点所在位置的简短相关信息
toLongString连接点所在位置的全部相关信息
getThis返回AOP代理对象,也就是com.sun.proxy.$Proxy18
getTarget返回目标对象(定义方法的接口或类)
getArgs()返回被通知方法参数列表
getSignature返回当前连接点签名,其getName()方法返回方法的FQN

执行所有的通知,观察环绕通知和前置通知和后置通知的先后顺序

 可以观察到,环绕通知先于before,后于after.

如果一个切点只含有一个通知,那么我们可以将切点的规则放在通知上

@Component
@Slf4j
@Aspect
public class LoginAspect {
    //前置通知
    @Before("execution(* com.javastudy.springaopdemo5.controller.LoginController.*(..))")
    public void doBefore() {
        log.info("do before....");
    }


}

4.Spring AOP 实现原理

Spring AOP 是构建在动态代理基础上,因此 Spring 对 AOP 的支持局限于方法级别的拦截。

Spring AOP 支持 JDK Proxy 和 CGLIB 方式实现动态代理。默认情况下,实现了接口的类,使

用 AOP 会基于 JDK 生成代理类,没有实现接口的类,会基于 CGLIB 生成代理类。

代理模式:

1.静态代理

1.定义接口
public interface PayService {
    void pay();
}
2.实现接口
public class AliPayService implements PayService{
    @Override
    public void pay() {
        System.out.println("ali pay...");
    }
}
3.创建代理类, 并同样实现支付接口
public class StaticProxy implements PayService {
    private final PayService payService;

    public StaticProxy(PayService payService) {
        this.payService = payService;
    }

    @Override
    public void pay() {
        System.out.println("before...");
        payService.pay();
        System.out.println("after...");
    }
}
4.实际使用
    public static void main(String[] args) {
        PayService service = new AliPayService();
        PayService proxy = new StaticProxy(service);
        proxy.pay();
    }

 静态代理有个很大的缺点,就是当有很多不同的接口的时候,我们需要定义很多个代理类实现不同的接口,当我们代理实现的功能相同的时候,但是有多个接口,此时完成这么多代理类很麻烦,此时需要我们的动态代理.

2.动态代理

1.JDK动态代理

从 JVM 角度来说,动态代理是在运行时动态生成类字节码,并加载到 JVM 中 的。

就 Java 来说,动态代理的实现方式有很多种,比如 JDK 动态代理、CGLIB 动态 代理等等。

定义JDK动态代理类

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

/**
 * @author Chooker
 * @create 2023-07-27 22:36
 */

public class JDKInvocationHandler implements InvocationHandler {
    //⽬标对象即就是被代理对象
    private Object target;

    public JDKInvocationHandler(Object target) {
        this.target = target;
    }
    //proxy代理对象

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //1.安全检查
        System.out.println("安全检查");
        //2.记录⽇志
        System.out.println("记录⽇志");
        //3.时间统计开始
        System.out.println("记录开始时间");
        //通过反射调⽤被代理类的⽅法
        Object retVal = method.invoke(target, args);
        //4.时间统计结束
        System.out.println("记录结束时间");
        return retVal;
    }
}

创建⼀个代理对象并使用 

    public static void main(String[] args) {
        PayService target = new AliPayService();
        //创建⼀个代理类:通过被代理类、被代理实现的接⼝、⽅法调⽤处理器来创建
        PayService proxy = (PayService) Proxy.newProxyInstance(
                target.getClass().getClassLoader(),
                new Class[]{PayService.class},
                new JDKInvocationHandler(target)
        );
        proxy.pay();
    }

缺点:JDK的动态代理必须有接口

2.CGLIB动态代理

CGLIB 动态代理类使用步骤

1. 定义一个类;

2. 自定义 MethodInterceptor 并重写 intercept 方法,intercept 用于拦截增强

被代理类的方法,和 JDK 动态代理中的 invoke 方法类似;

3. 通过 Enhancer 类的 create()创建代理类

添加依赖(如果创建的是一个Spring项目,不需要引入,因为Spring底层已经引入了cglib框架)

<dependency>
 <groupId>cglib</groupId>
 <artifactId>cglib</artifactId>
 <version>3.3.0</version>
</dependency>

自定义 MethodInterceptor(方法拦截器)

import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

/**
 * @author Chooker
 * @create 2023-07-27 22:48
 */
public class CGLIBInterceptor implements MethodInterceptor {
    //被代理对象
    private Object target;

    public CGLIBInterceptor(Object target) {
        this.target = target;
    }

    @Override
    public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        //1.安全检查
        System.out.println("安全检查");
        //2.记录⽇志
        System.out.println("记录⽇志");
        //3.时间统计开始
        System.out.println("记录开始时间");
        //通过cglib的代理⽅法调⽤
        Object retVal = methodProxy.invoke(target, args);
        //4.时间统计结束
        System.out.println("记录结束时间");
        return retVal;
    }


}

1. obj : 被代理的对象(需要增强的对象)

2. method : 被拦截的方法(需要增强的方法)

3. args : 方法入参

4. proxy : 用于调用原始方法

 创建代理类, 并使用 

    public static void main(String[] args) {
        PayService target = new AliPayService();
        PayService proxy = (PayService) Enhancer.create(target.getClass(), new CGLIBInterceptor(target));
        proxy.pay();
    }

JDK 动态代理和 CGLIB 动态代理对比

1. JDK 动态代理只能代理实现了接口的类或者直接代理接口,而 CGLIB 可以代 理未实现任何接口的类。

2. CGLIB 动态代理是通过生成⼀个被代理类的子类来拦截被代理类的方法调用,因此不能代理声明为 final 

性能: 大部分情况都是 JDK 动态代理更优秀,随着 JDK 版本的升级,这个优势更 加明显。

Spring代理选择

1. proxyTargetClass 为false, 目标实现了接口, 用jdk代理

2. proxyTargetClass 为false, 目标未实现接口, 用cglib代理

3. proxyTargetClass 为true, 用cglib代理

织入(Weaving):代理的生成时机
织入是把切面应用到目标对象并创建新的代理对象的过程,切面在指定的连接点被织入到目标对
象中。
在目标对象的生命周期里有多个点可以进行织入:

  • 编译期:切面在目标类编译时被织入。这种方式需要特殊的编译器。AspectJ的织入编译器就是以这种方式织入切面的。
  • 类加载期:切面在目标类加载到JVM时被织入。这种方式需要特殊的类加载器(ClassLoader),它可以在目标类被引入应用之前增强该目标类的字节码。AspectJ5的加载时织入(load-time weaving. LTW)就支持以这种方式织入切面。
  • 运行期:切面在应用运行的某⼀时刻被织入。⼀般情况下,在织入切面时,AOP容器会为目标对象动态创建⼀个代理对象。SpringAOP就是以这种方式织入切面的。

上面我们学习的是Spring AOP的原理,是接下来我们学习内容的底层,SpringBoot一些常见的功能进行了封装,底层使用AOP实现的

二.SpringBoot 统一功能处理

需要实现用户的登录权限的校验功能,在Servlet阶段,我们可以通过在Session中保存用户的信息,之后每个页面先通过session中判断是否存在用户的信息,如果存在说明用户已经登录过了,没有就跳转到登录的页面.

1.Spring AOP 用户统一登录验证的问题

我们第一时间想到的就是通过环绕通知来解决这个问题,可以对除了登录和注册的页面采用环绕通知,用来判断用户是否登录过了.但是会出现以下两个问题

1.. 没办法获取到 HttpSession 对象。

2. 我们要对一部分方法进行拦截,而另一部分方法不拦截,如注册方法和登录方法是不拦截的,这样 的话排除方法的规则很难定义,甚至没办法定义。

那么该如何解决呢?

2.Spring 拦截器

对于以上问题 Spring 中提供了具体的实现拦截器:HandlerInterceptor,拦截器的实现分为以下两个步骤:

1. 创建自定义拦截器,实现 HandlerInterceptor 接口的 preHandle(执行具体方法之前的预处理)方法。

2. 将自定义拦截器加入 WebMvcConfigurer 的 addInterceptors 方法中

1.自定义拦截器

@Component
public class LoginInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        HttpSession session = request.getSession(false);

        if (session != null && session.getAttribute("username") != null) {
            //通过
            return true;
        }
        //没有权限访问
        response.setStatus(401);
        return false;
    }
}

2.将自定义拦截器加入到系统配置

@Configuration
public class AppConfig implements WebMvcConfigurer {
    @Autowired
    private LoginInterceptor loginInterceptor;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginInterceptor).
                //表示拦截所有的路径
                addPathPatterns("/**").
                //不拦截login接口
                excludePathPatterns("/login").
                //不拦截register接口
                excludePathPatterns("/register");
    }
}

其中:

addPathPatterns:表示需要拦截的 URL,“**”表示拦截任意方法(也就是所有方法)。

excludePathPatterns:表示需要排除的 URL。

说明:以上拦截规则可以拦截此项目中的使用 URL,包括静态文件(图片文件、JS 和 CSS 等文件)。

排除所有的静态资源

    // 拦截器
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor())
                .addPathPatterns("/**") // 拦截所有接⼝
                .excludePathPatterns("/**/*.js")
                .excludePathPatterns("/**/*.css")
                .excludePathPatterns("/**/*.jpg")
                .excludePathPatterns("/login.html")
                .excludePathPatterns("/**/login"); // 排除接⼝
    }

拓展以下,可以在里面添加统一前缀的添加

@Configuration
public class AppConfig implements WebMvcConfigurer {
    @Autowired
    private LoginInterceptor loginInterceptor;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginInterceptor)
                .addPathPatterns("/**")// 拦截所有url
                .excludePathPatterns("/api/user/login")
                .excludePathPatterns("/api/user/reg");
    }

    @Override
    public void configurePathMatch(PathMatchConfigurer configurer) {
        configurer.addPathPrefix("api", c -> true);
    }
}

3.controller接口模仿登录

@RestController
@Slf4j
@RequestMapping("/user")
public class LoginController {

    @RequestMapping("/login")
    public boolean login(HttpServletRequest request, String username, String password) {
        log.info("login...");
        if (!StringUtils.hasLength(username) || !StringUtils.hasLength(password)) {
            return false;
        }
        //此时表示账号密码正确
        if ("admin".equals(username) && "123456".equals(password)) {
            HttpSession session = request.getSession(true);
            session.setAttribute("username", username);
            return true;
        }
        return false;

    }

    @RequestMapping("/register")
    public String register() {
        log.info("register...");
        return "register...";
    }

    @RequestMapping("/get")
    public String get() {
        log.info("get...");
        return "get...";
    }

}

当我们直接访问get接口的时候:显示的是401,表示没有权限

正常访问login和register接口都是可以实现的

 

 此时我们使用正确的账号密码登录:可以看到此时已经正确登录了

 此时我们再次访问get接口:可以看到此时正确访问

 3.拦截器的实现原理

正常情况下的调用顺序:

 然而有了拦截器之后,会在调用 Controller 之前进行相应的业务处理,执行的流程如下图所示:

拦截器是基于AOP的,Spring是基于Servlet的 

三.统一异常处理

统一异常处理使用的是 @ControllerAdvice + @ExceptionHandler 来实现的,@ControllerAdvice 表示控制器通知类,@ExceptionHandler 是异常处理器,两个结合表示当出现异常的时候执行某个通知, 也就是执行某个方法事件,具体实现代码如下
@ControllerAdvice
@ResponseBody
public class ErrorHandler {

    @ExceptionHandler(Exception.class)
    public Object error(Exception e) {
        HashMap<String, Object> map = new HashMap<>();
        map.put("success", 0);
        map.put("status", -1);
        map.put("msg", e.getMessage());
        return map;

    }

    @ExceptionHandler(NullPointerException.class)
    public Object error2(NullPointerException e) {
        HashMap<String, Object> result = new HashMap<>();
        result.put("success", 0);
        result.put("status", -2);

        result.put("message", "空指针异常:" + e.getMessage());
        return result;
    }

    @ExceptionHandler(ArithmeticException.class)
    public Object error2(ArithmeticException e) {
        HashMap<String, Object> result = new HashMap<>();
        result.put("success", 0);
        result.put("status", -3);

        result.put("message", "算数异常:" + e.getMessage());
        return result;
    }
}

controller

@RestController
@Slf4j
@RequestMapping("/error")
public class ErrorController {

    @RequestMapping("/test1")
    public boolean test1() {
        int i = 10 / 0;
        return true;
    }

    @RequestMapping("/test2")
    public boolean test2() {
        String a = null;
        a.length();
        return true;
    }

    @RequestMapping("/test3")
    public String test3() {
        throw new RuntimeException("test3手动创建异常");
    }
}
当有多个异常通知时,匹配顺序为当前类及其子类向上依次匹配

访问test1

访问test2

访问test3

可以观察到当错误异常为子类的时候,匹配顺序为当前类及其子类向上依次匹配

三.统一数据返回格式

1.为什么需要统一返回格式

统一数据返回格式的优点有很多,比如以下几个:

  1. 方便前端程序员更好的接收和解析后端数据接口返回的数据。
  2. 降低前端程序员和后端程序员的沟通成本,按照某个格式实现就行了,因为所有接口都是这样返回的。
  3. 有利于项目统一数据的维护和修改。
  4. 有利于后端技术部门的统一规范的标准制定,不会出现稀奇古怪的返回内容

2.统一数据返回格式的实现

统一的数据返回格式可以使⽤ @ControllerAdvice + ResponseBodyAdvice 的方式实现,具体实现代码如下:
@ControllerAdvice
public class ResponseHandler implements ResponseBodyAdvice {

    /**
     * 内容是否需要重写(通过此⽅法可以选择性部分控制器和⽅法进⾏重写)
     * 返回 true 表示重写
     */
    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
        return true;
    }
    /**
     * ⽅法返回之前调⽤此⽅法
     */
    @SneakyThrows
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        // 构造统⼀返回对象
        HashMap<String, Object> result = new HashMap<>();
        result.put("state", 1);
        result.put("msg", "");
        result.put("data", body);
        if(body instanceof String){
            ObjectMapper objectMapper = new ObjectMapper();
            return objectMapper.writeValueAsString(result);
        }
        return result;
    }
}

假如没有if((body instanceof String)这一段代码,会发生如下的错误

 controller:

@RestController
@Slf4j
@RequestMapping("/user")
public class LoginController {

    @RequestMapping("/login")
    public boolean login(HttpServletRequest request, String username, String password) {
        log.info("login...");
        if (!StringUtils.hasLength(username) || !StringUtils.hasLength(password)) {
            return false;
        }
        //此时表示账号密码正确
        if ("admin".equals(username) && "123456".equals(password)) {
            HttpSession session = request.getSession(true);
            session.setAttribute("username", username);
            return true;
        }
        return false;

    }

    @RequestMapping("/register")
    public String register() {
        log.info("register...");
        return "register...";
    }

    @RequestMapping("/get")
    public String get() {
        log.info("get...");
        return "get...";
    }

}

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Java基于SpringBoot的漫画网站,附源码,教程

博主介绍&#xff1a;✌程序员徐师兄、7年大厂程序员经历。全网粉丝30W、csdn博客专家、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ 今天为大家带来的是 基于SpringBootVue的漫画之家系统&#xff0c;亲测可用&#xff0c;可以作为课程设计&…

搜索关键词标红组件

搜索关键词标红功能 今年做的一个需求&#xff0c;先看效果图。 先讲一下实现方式&#xff0c;前端输入关键词查询&#xff0c;后端返回html模板&#xff0c;前端通过v-html渲染 查到的数据是分页式&#xff0c;v-html的样式需要使用/deep/声明 下面是组件代码 <template…

现货黄金与黄金一样吗?

在众多的黄金投资方式中&#xff0c;现货黄金可以说是集各家所长于一身的一种&#xff0c;它既承载了实物黄金的特性&#xff0c;能够发挥黄金抗通胀、避风险的重要作用&#xff0c;也拥有纸黄金无需交收的优点&#xff0c;更具有黄金期货可以双向交易、带有资金杠杆的高收益特…

python学习之【包和内置模块】

前言 接上篇文章 python学习之【模块】&#xff0c;这篇文章接着学习python中的包。 python中的包 包是一种用“点式模块名”构造Python模块命名空间的方法。在包中存放着一些功能相近的模块。 包的创建和导入 包的创建 我们可以在pytarm中创建一个package文件&#xff…

【Leetcode热题】打卡day1——10

目录 1、两数之和 - 哈希表 1、两数之和 - 哈希表 1. 两数之和 思路&#xff1a; 建立map&#xff0c;mp[nums[i]]i 存储值所对应的下标 顺序遍历每一个元素&#xff0c;先查找mp中是否存在与nums[i]匹配的值&#xff08;target-nums[i]&#xff09; 如果存在&#xff0c;则返…

如何使用Vcluster实现Kubernetes中的多租户

Kubernetes彻底改变了组织部署和管理容器化应用程序的方式&#xff0c;使跨集群编排和扩展应用程序变得更加容易。然而&#xff0c;在共享的Kubernetes集群上运行多个异构工作负载会带来资源争用、安全风险、缺乏定制和复杂管理等挑战。 以下几种方法可以在Kubernetes中实现隔离…

《golang设计模式》第二部分·结构型模式-05-门面模式Facade)

文章目录 1. 概述1.1 角色1.2 类图 2. 代码示例2.1 设计2.2 代码2.2 类图 1. 概述 门面&#xff08;Facade&#xff09;向客户端提供使用子系统的统一接口&#xff0c;用于简化客户端使用子系统的操作。 1.1 角色 门面角色&#xff08;Facade&#xff09; 客户端可以调用的接…

数字孪生和元宇宙:打造未来的数字边界

数字孪生和元宇宙是近两年来被热议的两个概念&#xff0c;但由于技术的交叉两者也极易被混淆。本文希望带大家深入探讨一下这两者之间的关系&#xff0c;以及它们如何一起构建了数字时代的新格局。 1. 数字孪生的本质 数字孪生是一种虚拟模型&#xff0c;它通过数字手段对现实…

在微信公众号怎么实现投票活动

微信公众号实现投票活动的方法和步骤 一、投票活动的优势 通过投票活动&#xff0c;微信公众号可以实现用户参与、增加互动、了解用户需求等功能&#xff0c;同时也可以提升品牌知名度和用户粘性。以下是一些投票活动的优势&#xff1a; 增加用户参与度&#xff1a;通过投票活…

Seata四种事务模式AT、TCC、SAGA 、 XA详解

文章目录 一、 GlobalTransactional一、AT模式1、原理2 、优缺点3、实现 二、TCC模式1、原理2 、优缺点3、实现 三、SAGA模式1 、原理2 、优缺点3 、实现 四、XA模式1 、原理2 、优缺点3、 实现 五、 四种模式对比 我们知道Seata是一款开源的分布式事务解决方案&#xff0c;致力…

【LeetCode75】第五十八题 组合总和Ⅲ

目录 题目&#xff1a; 示例&#xff1a; 分析&#xff1a; 代码&#xff1a; 题目&#xff1a; 示例&#xff1a; 分析&#xff1a; 题目让我们找出长度为k并且总和为n的所有组合&#xff0c;我们能用的元素只有1~9。 那么这种要求排列组合的题&#xff0c;基本是是离不…

2023客服管理者面临的挑战

客服管理者在当今的数字化时代也面临着许多挑战。以下是一些主要的挑战&#xff1a; 同行业竞争加剧&#xff1a;客服行业面临着来自同行业的竞争压力。为了获得竞争优势&#xff0c;企业需要不断提高自身的产品和服务质量&#xff0c;同时还需要不断降低成本、提高效率。然而…

go调用so库

来源&#xff1a;微信公众号「编程学习基地」 文章目录 go调用so库生成so库调用so库 go调用so库 生成so库 load_so.h #ifndef _LOAD_SO_H #define _LOAD_SO_Hint do_test_so_func(int a,int b);#endif load_so.c #include "load_so.h"int do_test_so_func(int …

数据结构——四叉树

四叉树&#xff08;Quadtree&#xff09;是一种用于表示和管理二维空间的树状数据结构。它将二维空间递归地分割成四个象限&#xff0c;每个象限可以继续分割&#xff0c;以实现对空间的更精细的划分。四叉树通常用于解决空间搜索和查询问题&#xff0c;例如碰撞检测、图像压缩…

vector使用和模拟实现

&#x1f493;博主个人主页:不是笨小孩&#x1f440; ⏩专栏分类:数据结构与算法&#x1f440; C&#x1f440; 刷题专栏&#x1f440; C语言&#x1f440; &#x1f69a;代码仓库:笨小孩的代码库&#x1f440; ⏩社区&#xff1a;不是笨小孩&#x1f440; &#x1f339;欢迎大…

2.6W字系统总结,带你实现 Linux 自由!

以前就聊过大多数应届生入职后会遇到的两大拦路虎分别是Git和Linux&#xff0c;其中关于Git&#xff0c;前段时间已经分享过自己工作两年多以来用过的比较实用的一些命令了&#xff0c;这些命令可能不是最花哨的&#xff0c;但绝对是最实用的。 今天分享一份很全面的Linux常用…

springboot+canal+mysql+redis缓存双写一致性

canal官网地址&#xff1a;https://github.com/alibaba/canal/wiki/QuickStart 基本上按照官网的步骤来就行 准备 首先服务器上要安装好jdk&#xff0c;因为canal运行需要jdk,同时把canal对应的端口在服务中开放&#xff0c;否则连接不上 对于自建 MySQL , 需要先开启 Binl…

linux安装python3.x版本

linux安装python3.x版本 ① 安装依赖环境② 下载python版本包③ 安装python④ 建立软链接⑤ 加入path⑥ 验证 官网版本地址&#xff1a;https://www.python.org/ftp/python/ ① 安装依赖环境 yum -y install zlib-devel bzip2-devel openssl-devel ncurses-devel sqlite-devel…

Vue 使用vue-pdf 显示pdf文件 切换页面 缩放 全屏 自动播放等

<template><div id"container"><!-- 上一页、下一页--><div class"right-btn"><div click"toFullOrExit" class"turn-btn"><span>{{ isFull 1 ? "取消全屏" : "全屏" }}&l…

GLTF-pipeline

gltf-pipeline可用作命令行工具或 Node.js 模块。 开始 安装 Node.js如果还没有&#xff0c;然后&#xff1a; npm install -g gltf-pipeline使用 gltf-pipeline 作为命令行工具&#xff1a; 将 glTF 转换为 glb gltf-pipeline -i model.gltf -o model.glb gltf-pipeline…