当前位置: 首页 > news >正文

设计模式 三、结构型设计模式

一、代理模式

        代理设计模式(Proxy Design Pattern)是一种结构型设计模式,它为其他对象提供了一个代理,以控制对这个对象的访问。 代理模式可以用于实现懒加载、安全访问控制、日志记录等功能。简单来说,代理模式 就是通过代理对象来控制对实际对象的访问,代理对象在客户端和目标对象之间起到了中介的作用。
        在设计模式中,代理模式可以分为静态代理和动态代理。静态代理是指代理类在编译时就已经确定,而动态代理是指代理类在运行时动态生成。

        

        1、静态代理

        静态代理是在代码编译阶段就已经生成了代理类,代理类需要实现目标对象相同的接口。
        优点:可以在不修改目标对象的前提下对目标对象进行增强。
        缺点:需要为每个目标创建一个代理类,导致系统中类的数量增加,维护成本较高。

        2、静态代理的使用场景

        2.1 缓存代理

        缓存代理是一种特殊类型的的代理模式,它可以为耗时的操作或者重复的请求提供缓存功能,从而提高程序的执行效率。缓存代理通常会在内部维护一个缓存的数据结构。如HashMap 或者 LinkHashMap,用来 存储已经处理过的请求及其结果。

        以下是一个示例:

        假设有一个数据查询接口,它从数据库或者其他数据源中检索数据,在没有缓存代理的情况下,每次查询都需要访问数据库,这可能会导致较高的资源消耗和延迟,通过引入缓存代理,我们可以将查询结果存储在内存中,从而避免重复查询数据库。

        首先定义一个数据查询接口:

public interface DataQuery {String query(String queryKey);
}

        然后,实现一个真实的数据查询类:

public class DatabaseDataQuery implements DataQuery {@Overridepublic String query(String queryKey) {// 查询数据库并返回结果return "Result from database: " + queryKey;}
}

        然后创建一个缓存代理类,它实现DataQuery接口,并在内部使用HashMap作为缓存:

public class CachingDataQueryProxy implements DataQuery {private final DataQuery realDataQuery;private final Map<String, String> cache;public CachingDataQueryProxy(DataQuery realDataQuery) {this.realDataQuery = new DatabaseDataQuery();cache = new HashMap<>();}@Overridepublic String query(String queryKey) {String result = cache.get(queryKey);if (result == null) {result = realDataQuery.query(queryKey);cache.put(queryKey, result);System.out.println("Result retrieved from database and added to cache.");} else {System.out.println("Result retrieved from cache.");}return result;}
}

        最后,我们在客户端就可以使用缓存代理:

public class Client {public static void main(String[] args) {DataQuery realDataQuery = new DatabaseDataQuery();DataQuery cachingDataQueryProxy = newCachingDataQueryProxy(realDataQuery);String queryKey = "example_key";// 第一次查询,从数据库中获取数据并将其缓存System.out.println(cachingDataQueryProxy.query(queryKey));// 第二次查询相同的数据,从缓存中获取System.out.println(cachingDataQueryProxy.query(queryKey));}
}

        其实也就是说我们单独实现了一个方法,我们在这个方法中取调用原本的查询操作,也就是在这个新的方法中完成了过滤的操作。

        2.2 安全代理

        (Security Proxy)是一种代理模式用的应用,它用于控制对真实主题对象的访问。通过安全代理,可以实现访问控制、权限控制等安全相关功能。
       
        以下是一个简单的安全代理示例:
       
        假设我们有一个敏感数据查询接口,只有具有特定权限的用户才能访问:
        首先,我们定义一个数据查询接口:

public interface SensitiveDataQuery {String queryData(String userId);
}

        接着实现一个真的敏感数据查询类:

public class SensitiveDataQueryImpl implements SensitiveDataQuery {@Overridepublic String queryData(String userId) {// 查询敏感数据并返回结果return "Sensitive data for user: " + userId;}
}

        然后,我们创建一个安全代理类,它实现了 SensitiveDataQuery接口,并在内部进行权限验证:

public class SecurityProxy implements SensitiveDataQuery {private final SensitiveDataQuery sensitiveDataQuery;private final UserAuthenticator userAuthenticator;public SecurityProxy(SensitiveDataQuery sensitiveDataQuery,UserAuthenticator userAuthenticator) {this.sensitiveDataQuery = sensitiveDataQuery;this.userAuthenticator = userAuthenticator;}@Overridepublic String queryData(String userId) {if (userAuthenticator.hasPermission(userId)) {return sensitiveDataQuery.queryData(userId);} else {return "Access Denied: Insufficient permission for user" + userId;}}
}

        我们使用一个UserAuthenticator类来模拟用户权限验证:

public class UserAuthenticator {private final List<String> authorizedUserIds;public UserAuthenticator() {// 模拟从数据库或配置文件中获取已授权的用户列表authorizedUserIds = Arrays.asList("user1", "user2", "user3");}public boolean hasPermission(String userId) {return authorizedUserIds.contains(userId);}
}

        在客户端中调用:

public class Client {public static void main(String[] args) {SensitiveDataQuery sensitiveDataQuery = new SensitiveDataQueryImpl();UserAuthenticator userAuthenticator = new UserAuthenticator();SensitiveDataQuery securityProxy = new SecurityProxy(sensitiveDataQuery,userAuthenticator);String userId1 = "user1";String userId2 = "user4";// 用户1具有访问权限System.out.println(securityProxy.queryData(userId1));// 用户4没有访问权限System.out.println(securityProxy.queryData(userId2));}
}

        2.3 虚拟代理

        (Virtual Proxy)是一种代理模式,用于在需要时延迟创建耗时或资源密集型对象。虚拟代理在初始访问时才创建实际对象,之后将直接使用该对象。这可以避免在实际对象尚未使用的情况下就创建它,从而节省资源。

        例如:我们有一个大型图片类,他从网络加载图像。由于图像可能非常大。我们希望在需要显示时才加载他,为了实现这一点,我们可以创建一个虚拟代理来代表大型图片类,首先定义一个图片接口:

public interface Image {void display();
}

        然后实现了一个大型图片类,他从网络加载图像并实现display() 方法:

public class LargeImage implements Image {private final String imageUrl;public LargeImage(String imageUrl) {this.imageUrl = imageUrl;loadImageFromNetwork();}private void loadImageFromNetwork() {System.out.println("Loading image from network: " + imageUrl);// 真实的图像加载逻辑...}@Overridepublic void display() {System.out.println("Displaying image: " + imageUrl);}
}

         然后创建一个虚拟代理类,它实现了Image接口,并在内部使用LargeImage:

public class VirtualImageProxy implements Image {private final String imageUrl;private LargeImage largeImage;public VirtualImageProxy(String imageUrl) {this.imageUrl = imageUrl;}@Overridepublic void display() {if (largeImage == null) {largeImage = new LargeImage(imageUrl);}largeImage.display();}
}

        最后我们在客户端中使用虚拟代理:

public class Client {public static void main(String[] args) {Image virtualImageProxy = newVirtualImageProxy("https://example.com/large-image.jpg");System.out.println("Image will not be loaded until it is displayed.");// 调用 display() 方法时,才会创建并加载大型图片virtualImageProxy.display();}
}

        这个例子就是通过虚拟代理实现懒加载,以减少资源消耗和提高程序性能,当实际对象的创建和初始化非常耗时或占用大量资源时,虚拟代理是一个很好的选择。

        2.4 远程代理

        (Remote Proxy)是一种代理模式,用于访问位于不用的地址空间的对象。远程代理可以为本地对象提供与远程对象相同的接口,使得客户端可以透明的访问远程对象。通常,远程代理需要处理网络通信、序列化和反序列化等细节。以后做rpc时也会使用。
        
        简单的示例:首先定义一个服务接口:

public interface RemoteService {String fetchData(String dataId);
}

        然后实现一个远程服务类,在服务端运行并实现fetchData()方法:

public class RemoteServiceImpl implements RemoteService {@Overridepublic String fetchData(String dataId) {// 实际操作,例如从数据库获取数据return "Data from remote service: " + dataId;}
}

·        接下来,我们创建了一个远程代理类,它实现了RemoteService接口,并在内部处理网络通信等细节:

public class RemoteServiceProxy implements RemoteService {private final String remoteServiceUrl;private RemoteService remoteService;public RemoteServiceProxy(String remoteServiceUrl) {this.remoteServiceUrl = remoteServiceUrl;this.remoteService = new RemoteService();}@Overridepublic String fetchData(String dataId) {// 网络通信、序列化和反序列化等逻辑System.out.println("Connecting to remote service at: " + remoteServiceUrl);// 假设我们已经获取到远程服务的数据String result = remoteService.fetchData(dataId);System.out.println("Received data from remote service.");return result;}
}

        

        2.5 静态代理步骤总结

        通过前面几个案例,大致了解了静态代理的使用方式,其大致流程如下:
        1.创建一个接口,定义代理类和被代理类共同实现的方法。
        2.创建被代理类,实现这个接口,并且在其中定义实现方法。
        3.创建代理类,也要实现这个接口,同时在其中定义一个被代理类的对象作为成员变量。
        4.在代理类中实现接口中的方法,方法中调用被代理类中的对应方法。
        5.通过创建代理对象,并调用其方法,方法增强。

        这样,被代理类的方法就会被代理类所覆盖,实现了对被代理类的增强或修改。

        在静态代理中,也可以使用继承来实现代理,具体步骤如下:
        1.创建被代理类,定于需要被代理的方法。
        2.创建代理类,继承被代理类,重写被代理类中的方法,对方法进行增强。
        3.再重写的方法中添加代理逻辑,例如在调用被代理类中的方法前后添加日志记录、安全检查等功能。
        4.在使用代理类时,创建代理对象,调用重写方法。
        这样,被代理类的方法就会被代理类所覆盖,实现了对被代理类的增强或修改。使用继承来实现代理类的好处就是简单易懂,不需要创建接口,同时继承可以继承被代理类的属性和方法,可以更方便的访问类中的成员。但是这个方法也有一些缺点,类如代理类与被代理类的耦合度较高,不够灵活。

        3、动态代理

        java中动态代理的实现方式主要有两种:基于JDK的动态代理和基于CGLB的动态代理。
       
        动态代理是指在程序运行时动态生成代理类,无需手动编写代理类,大大降低了代码的复杂度,动态代理一般使用Java提供的反射机制实现,可以对任意实现了接口的类进行代理。动态代理的优点是灵活性高,可以根据需要动态生成代理类,缺点是性能相对较低,由于使用反射机制,在运行时会产生额外的开销。
        静态代理 和 动态代理都是代理模式的实现方式,其主要区别在于代理类的生成时机和方式。静态代理需要手动编写代理类,适用于代理类数量较少,不需要频繁修改的场景。而动态代理不需要手动编写代理类,可以动态的生成代理类,适用于代理类数量较多、需要频繁修改的场景。

       

        4.1 基于JDK的动态代理实现步骤 

        基于JDK的动态代理需要使用 java.lang.reflect.Proxy 类 和 Java.lang.reflect.InvocationHandler 接口。我们依旧使用上述的缓存代理的案例来实现,具体步骤如下:
       
        1)定义一个接口,声明需要代理的方法:

public interface DataQuery {String query(String queryKey);String queryAll(String queryKey);
}

        2)创建一个被代理类,实现这个接口,并在其中定义实现方法:

public class DatabaseDataQuery implements DataQuery {@Overridepublic String query(String queryKey) {// 他会使用数据源从数据库查询数据很慢System.out.println("正在从数据库查询数据");return "result";}@Overridepublic String queryAll(String queryKey) {// 他会使用数据源从数据库查询数据很慢System.out.println("正在从数据库查询数据");return "all result";}
}

        3)创建一个代理类,实现了InvocationHandler接口,并在其中定义一个被代理类的对象作为属性。

public class CacheInvocationHandler implements InvocationHandler {private HashMap<String,String> cache = new LinkedHashMap<>(256);private DataQuery databaseDataQuery;public CacheInvocationHandler(DatabaseDataQuery databaseDataQuery) {this.databaseDataQuery = databaseDataQuery;}public CacheInvocationHandler() {this.databaseDataQuery = new DatabaseDataQuery();}@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {// 1、判断是哪一个方法String result = null;if("query".equals(method.getName())){// 2、查询缓存,命中直接返回result = cache.get(args[0].toString());if(result != null){System.out.println("数据从缓存重获取。");return result;}// 3、未命中,查数据库(需要代理实例)result = (String) method.invoke(databaseDataQuery, args);// 4、如果查询到了,进行呢缓存cache.put(args[0].toString(),result);return result;}// 当其他的方法被调用,不希望被干预,直接调用原生的方法return method.invoke(databaseDataQuery,args);}
}

        在代理类中,我们实现了InvocationHandler接口,并在其中定义了一个被代理类的对象作为属性。在invoke方法中,我们可以对被代理对象的方法进行增强,并在方法调用前后输出日志。

        4)使用代理类,创建被代理类的对象和代理类的对象,并使用Proxy.newProxyInstance方法生成代理对象。

public class Main {public static void main(String[] args) {// jdk提供的代理实现,主要是使用Proxy类来完成// 1、classLoader:被代理类的类加载器ClassLoader classLoader = Thread.currentThread().getContextClassLoader();// 2、代理类需要实现的接口数组Class[] interfaces = new Class[]{DataQuery.class};// 3、InvocationHandlerInvocationHandler invocationHandler = new CacheInvocationHandler();DataQuery dataQuery = (DataQuery)Proxy.newProxyInstance(classLoader, interfaces, invocationHandler);// 事实上调用query方法的使用,他是调用了invokeString result = dataQuery.query("key1");System.out.println(result);System.out.println("--------------------");result = dataQuery.query("key1");System.out.println(result);System.out.println("--------------------");result = dataQuery.query("key2");System.out.println(result);System.out.println("++++++++++++++++++++++++++++++++++++");// 事实上调用queryAll方法的使用,他是调用了invokeresult = dataQuery.queryAll("key1");System.out.println(result);System.out.println("--------------------");result = dataQuery.queryAll("key1");System.out.println(result);System.out.println("--------------------");result = dataQuery.queryAll("key2");System.out.println(result);System.out.println("--------------------");}
}


        再看一下生成日志的例子:

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;interface UserService {void saveUser(String username);
}class UserServiceImpl implements UserService {public void saveUser(String username) {System.out.println("Saving user: " + username);}
}class LogProxy implements InvocationHandler {private Object target;public LogProxy(Object target) {this.target = target;}public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {System.out.println("Before method: " + method.getName());Object result = method.invoke(target, args);System.out.println("After method: " + method.getName());return result;}
}public class DynamicProxyExample {public static void main(String[] args) {UserService userService = new UserServiceImpl();UserService proxy = (UserService) Proxy.newProxyInstance(userService.getClass().getClassLoader(),userService.getClass().getInterfaces(),new LogProxy(userService));proxy.saveUser("Alice");}
}

        其实主要的步骤就是要编写我们的动态代理类,并在invoke 方法中书写自己需要添加的逻辑,Proxy.newProxyInstance 方法生成代理对象,这一步很多人看不懂,其实不需要担心,因为这个其实就是一个固定的api知道怎么调用即可!

        4.2 基于CGLIB的动态代理实现步骤

        基于CGLB的动态代理需要使用 net.sf.cglib.proxy.Enhancer类 和 net.sf.cglib.proxy.MethodInterceptor 接口 ,具体步骤如下:

        1)创建一个被代理类,定义需要被代理的方法:

public class DatabaseDataQuery {public String query(String queryKey) {// 他会使用数据源从数据库查询数据很慢System.out.println("正在从数据库查询数据");return "result";}public String queryAll(String queryKey) {// 他会使用数据源从数据库查询数据很慢System.out.println("正在从数据库查询数据");return "all result";}
}

        2)创建一个方法拦截器类,实现MethodInterceptor接口,并在其中定义一个被代理类的对象作为属性。

public class CacheMethodInterceptor implements MethodInterceptor {private HashMap<String,String> cache = new LinkedHashMap<>(256);private DatabaseDataQuery databaseDataQuery;public CacheMethodInterceptor() {this.databaseDataQuery = new DatabaseDataQuery();}@Overridepublic Object intercept(Object obj, Method method, Object[] args,MethodProxy proxy) throws Throwable {// 1、判断是哪一个方法String result = null;if("query".equals(method.getName())){// 2、查询缓存,命中直接返回result = cache.get(args[0].toString());if(result != null){System.out.println("数据从缓存重获取。");return result;}// 3、未命中,查数据库(需要代理实例)result = (String) method.invoke(databaseDataQuery, args);// 4、如果查询到了,进行呢缓存cache.put(args[0].toString(),result);return result;}return method.invoke(databaseDataQuery,args);}
}

        在这个代理类中,我们实现了MethodInterceptor 接口,并在其中定义了一个被代理类的对象作为属性,在intercept方法中,我们可以对被代理对象的方法进行增强,并在方法调用前后输出日志。

        3)在使用代理类时,创建被代理类的对象和代理类的对象,并使用Enhancer.create方法生成代理对象。

public class Main {public static void main(String[] args) {// cglib通过EnhancerEnhancer enhancer = new Enhancer();// 设置他的父类enhancer.setSuperclass(DatabaseDataQuery.class);// 设置一个方法拦截器,用来拦截方法enhancer.setCallback(new CacheMethodInterceptor());// 创建代理类DatabaseDataQuery databaseDataQuery =(DatabaseDataQuery)enhancer.create();databaseDataQuery.query("key1");databaseDataQuery.query("key1");databaseDataQuery.query("key2");}
}

       实现打印日志的例子:

class UserServiceImpl {public void saveUser(String username) {System.out.println("Saving user: " + username);}
}class LogInterceptor implements MethodInterceptor {public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {System.out.println("Before method: " + method.getName());Object result = proxy.invokeSuper(obj, args);System.out.println("After method: " + method.getName());return result;}
}public class CglibProxyExample {public static void main(String[] args) {Enhancer enhancer = new Enhancer();enhancer.setSuperclass(UserServiceImpl.class);enhancer.setCallback(new LogInterceptor());UserServiceImpl proxy = (UserServiceImpl) enhancer.create();proxy.saveUser("Bob");}
}

        在这个示例中,我们使用 Enhancer.create 方法生成代理对象,并将代理对象转成RealSubject类型,以便调用request方法,在代理对象调用request方法时,会调用DynamicProxy类中的intercept方法,实现对被代理对象的增强。
        在实际应用中,基于CGLIB的动态代理可以代理任意类,但是生成的代理类比较重量级。如果被代理类是一个接口,建议使用基于JDK的动态代理来实现,这也是spring的做法;如果被代理类没有实现接口或者需要代理的方法时final方法,建议使用基于CGLIB的动态代理来实现。

        4.3 spring aop 与 动态代理之间的关系

        spring aop 即面向切面编程,他对aop进行了封装,使用面向对象的思想来实现,所以aop的底层使用动态代理来实现的。

        4、动态代理的应用场景

        4.1  日志记录:使用动态代理可以在方法调用前后自动添加日志记录,从而跟踪方法的执行过程。这样可以方便的监控系统运行情况,诊断问题,而无需修改实际类的代码。

        4.2 性能监控:动态代理可用于测量方法执行的时间,以评估性能。在方法调用前后记录时间戳,然后计算时间差,就可以得到方法执行所需要的时间。

        4.3 事务管理:在数据库操作中,动态代理可用于自动管理事务。在方法调用前开始一个事务,在方法成功执行后提交事务,如果发生异常,则回滚事务,这样可以确保数据的一致性和完整性。

        4.4 权限验证:使用动态代理可以在方法调用前进行权限验证,确保具有适当权限的用户才能访问受保护的资源。这可以提高系统的安全性。

        4.5缓存:动态代理可用于实现方法结果的的缓存。在方法调用前检查缓存,如果缓存中有结果,则直接返回,否则执行方法并将结果存入缓存。这样可以提高程序的执行效率。

        4.6 负载均衡与故障转移:在分布式系统中,动态代理可以用于实现负载均衡和故障转移。代理对象根据某种策(入轮询、随机等)选择一个可用的服务实例。并将请求转发给它。如果服务实例发生故障,代理对象可以自动选择另一个可用的实例。

        4.7 API速率限制:使用动态代理,可以在方法调用前检查aoi请求速率是否超过预设的限制。如果超过限制,可以拒绝请求获奖请求延迟一段时间后再执行。

        4.8 数据验证:在方法调用前,动态代理可以用于验证传入的参数是否符合预期的规则和约束。这有助于确保数据的有效性和一致性。

        4.9 重试机制:当方法调用失败时,比如网络问题等等,动态代理可以实现自动重试的机制,代理对象可以在一定的时间间隔内尝试重新执行方法,直到成功达到最大重试次数。

        4.10 懒加载与资源管理:动态代理可以用于实现资源的懒加载和管理。例如,代理对象可以在第一次访问资源时才创建和初始化它。此外,代理对象还可以在资源不再需要时自动释放它,以减少内存占用和提高性能。

        4.11 跨语言和跨平台调用:动态代理可以实现跨语言和跨平台的对象调用。例如,一个java客户端可以使用动态代理调用一个基于python的服务,在这种情况下,代理对象会负责跨语言通信的细节,如序列化、反序列化和网络传输。

        4.12 AOP(面向切面编程):动态代理是实现AOP的一种方式。AOP允许在程序运行时动态的插入和修改横切关注点(如日志记录、性能监控等),而无需修改实际代码。动态代理可以轻松地实现AOP,以提高代码的可维护性和可重用性。

        具体例子省略,可自行查询相关例子。

二、装饰器模式

        1、实现原理

        装饰器设计模式是一种结构型设计模式,它允许动态的为对象添加新的行为,它通过创建一个包装器来实现。即将对象放入一个装饰器中,再将装饰器类放入另一个装饰器类中,以此类推,形成一个包装链。这样我们可以在不改变原有对象的情况下 ,动态的添加新的行为或修改原有行为。

        实现步如下:

        1)定义一个 接口或者抽象类,作为被装饰对象的基类

        例如创建一个component的接口,包含一个名为operation 的抽象方法,用于定义被装饰对象的基本行为。

public interface Component {void operation();
}

         2)定义一个具体的被装饰对象,实现基类中的方法

public class ConcreteComponent implements Component {@Overridepublic void operation() {System.out.println("ConcreteComponent is doing something...");}
}

        3)定义一个抽象装饰类,继承基类,并将被装饰对象作为属性

public abstract class Decorator implements Component {protected Component component;public Decorator(Component component) {this.component = component;}@Overridepublic void operation() {component.operation();}
}

        4)定义具体的装饰器类,继承抽象装饰器类,并实现增强逻辑

public class ConcreteDecoratorA extends Decorator {public ConcreteDecoratorA(Component component) {super(component);}@Overridepublic void operation() {super.operation();System.out.println("ConcreteDecoratorA is adding new behavior...");}
}

        5)使用装饰器增强被装饰对象

public class Main {public static void main(String[] args) {Component component = new ConcreteComponent();component = new ConcreteDecoratorA(component);component.operation();}
}

        上面的过程跟静态代理有一些相似之处,但他们之间还是有区别的:

        代理模式的目的是为了控制对象的访问,他在对象的外部提供了一个代理对象来控制对原始对象的访问。代理对象和原始对象通常实现同一个接口或继承同一个类,以保证二者可以互相替代。
        装饰器模式的目的是为了动态地增强对象的功能,他在对象地内部通过一种包装起的方式来实现。装饰器模式中,装饰器类和被装饰对象通常实现同一个接口或继承同一个类,以保证二者可以互相替代。装饰器模式也称为包装器模式。

        虽然这两个模式它们实现方式上都是通过继承统一地接口,然后在代理类和装饰者类中通过构造函数引入被代理类和被装饰者类,但是装饰着模式和静态代理模式在设计理念上有着本质的区别。装饰者模式注重于动态地为对象添加新功能,而静态代理模式则注重于对对象访问地控制。

        2、使用场景

        在Java中,装饰器模式的应用非常广泛,特别是在IO操作中。Java的IO类库就是使用装饰器模式来实现不同的数据流之间的转换和增强的。

        2.1 从IO库的设计理解装饰器

       例如,我们要打开文件 test.txt 从中读取数据,其中,InputStream 是一个抽象类,FileInputStream 是专门用来读取文件流的子类。BufferedInputStream 是一个支持带缓存功能的数据读取类,可以提高数据读取的效率,具体代码如下:

    InputStream in = new FileInputStream("D:/test.txt");InputStream bin = new BufferedInputStream(in);byte[] data = new byte[128];while (bin.read(data) != -1) {//...}

        初看上面的代码可能会觉得麻烦,需要先创建一个FileInputStream对象,然后传递给BufferedInputStream对象来使用。之前会觉得为什么不设计一个 继承FileInputStream对象并且支持缓存的BufferedFileStream类呢? 
        其实如果我们使用继承的方式来实现的话,就需要继续派生出DataFileInputStream、DataPipedInputStream 等类。如果我们还需要既支持缓存、又支持按照基本类型读取数据的类,那就要再继续派生出 BufferedDataFileInputStream、BufferedDataPipedInputStream n 类,这还只是附加了两个增强功能,如果还需要附加更多功能,那就会导致爆炸,类继承结构变得无比复杂,代码即不好扩展,也不好维护。这就是之前再讲设计原则的时候,不推荐使用继承的原因。

        所以使用装饰器模式来设计IO类会比较好,时间上IO类也确实是这么设计的,但是并不是简单的套用了装饰器的设计模式。
        使用装饰器模式第一个比较特殊的地方:装饰器类和原始类继承同样的父类,这样我们可以对原始类 嵌套 多个装饰器类。
        第二个比较特殊的地方:装饰器类是对功能的增强,这也是装饰器模式应用场景的一个重要特点。

        2.2 mybatis的缓存设计

        mybatis中一级缓存 二级缓存,都是使用装饰器模式实现的,具体省略,可以自行查询以下源码。

        3、总结

        装饰器模式主要解决继承关系过于复杂的问题,通常是通过组合来替代继承。它主要的作用是给原始类添加增强功能。这也是判断是否该用装饰器模式的一个重要的依据。除此之外,装饰器模式还有一个特点,那就是可以对原始类嵌套使用多个装饰器。为了满足这个应用场景,在设计的时候,装饰器类需要跟原始类继承相同的抽象类或者接口。

        对于大多数 添加缓存 的业务场景,核心目的主要就是相增强对象的功能(即增加缓存功能),而不是控制对象的访问,所以装饰器模式可能会更合适。但是比如相对持久层增加一个本地缓存,代理设计模式也是很好的选择。

三、桥接模式

        桥接模式的代码实现非常简单,但是理解起来稍微有点难度,并且应用场景也比较局限,相对于代理模式来说,桥接模式在实际的项目中并没有那么常用,只需要简单了解,见到能认识就行。

        1、桥接模式示例

        JDBC驱动是桥接模式的经典应用,复习一下JDBC驱动来查询数据库方法,具体代码如下所示:

    // 1.数据库连接的4个基本要素:String url = "jdbc:mysql://127.0.0.1:3306/ydlclass?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai";String user = "root";String password = "root";// 8.0之后名字改了 com.mysql.cj.jdbc.DriverString driverName = "com.mysql.cj.jdbc.Driver";// 2.实例化Driver,可省略// 3.注册驱动,可省略// 4.获取连接conn = DriverManager.getConnection(url, user, password);

        如果要把mysql数据库换成Oracle数据库,只需要把驱动com.mysql.cj.jdbc.Driver 换成 oracle.jdbc.driver.OracleDriver,同时将url进行替换就可以了。
        在工作中上述配置一般在配饰文件中,一般直接修改配置文件即可。
        不管是改代码还是改配置,在项目中,从一个数据库切换到另一个数据库,都只需要改动很少的代码。或者完全不需要改动代码。这个切换模式的设计要看 com.mysql.cj.jdbc.driver这个类的相关代码。

public class Driver extends NonRegisteringDriver implements java.sql.Driver {//// Register ourselves with the DriverManager//static {try {java.sql.DriverManager.registerDriver(new Driver());} catch (SQLException E) {throw new RuntimeException("Can't register driver!");}}/*** Construct a new driver and register it with DriverManager* @throws SQLException if a database error occurs.*/public Driver() throws SQLException {// Required for Class.forName().newInstance()}
}

        结合 com.mysql.jdbc.Driver 的代码实现,可以发现当执行 Class.forName("com.mysql.jdbc.Driver")这条语句的时候,实际上做了两件事情。
        第一件事情是要求JVM查找并加载指定的Driver类。
        第二件事情是执行该类的静态代码,也就是将mysql Driver 注册到DriverManager类中。
        当我们把具体的Driver实现类(比如com.mysql.cj.jdbc.Driver)注册到DriverManager之后,后续所有对JDBC接口的调用,都会委派到对具体的Driver实现类来执行。而Driver实现类都实现了相同的接口(java.sql.Drvier),这也是可以灵活切换Driver的原因。

        以下是具体代码:

public class DriverManager {// List of registered JDBC driversprivate static final CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();public static void registerDriver(java.sql.Driver driver,DriverAction da) throws SQLException {/* Register the driver if it has not already been added to our list */if (driver != null) {registeredDrivers.addIfAbsent(new DriverInfo(driver, da));} else {// This is for compatibility with the original DriverManagerthrow new NullPointerException();}println("registerDriver: " + driver);}private static Connection getConnection(String url, java.util.Properties info, Class<?> caller) throws SQLException {/** When callerCl is null, we should check the application's* (which is invoking this class indirectly)* classloader, so that the JDBC driver class outside rt.jar* can be loaded from here.*/ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;if (callerCL == null || callerCL == ClassLoader.getPlatformClassLoader()) {callerCL = Thread.currentThread().getContextClassLoader();}if (url == null) {throw new SQLException("The url cannot be null", "08001");}println("DriverManager.getConnection(\"" + url + "\")");ensureDriversInitialized();// Walk through the loaded registeredDrivers attempting to make a connection.// Remember the first exception that gets raised so we can reraise it.SQLException reason = null;for (DriverInfo aDriver : registeredDrivers) {// If the caller does not have permission to load the driver then// skip it.if (isDriverAllowed(aDriver.driver, callerCL)) {try {println(" trying " + aDriver.driver.getClass().getName());Connection con = aDriver.driver.connect(url, info);if (con != null) {// Success!println("getConnection returning " + aDriver.driver.getClass().getName());return (con);}} catch (SQLException ex) {if (reason == null) {reason = ex;}}} else {println(" skipping: " + aDriver.driver.getClass().getName());}}// if we got here nobody could connect.if (reason != null) {println("getConnection failed: " + reason);throw reason;}println("getConnection: no suitable driver found for "+ url);throw new SQLException("No suitable driver found for "+ url, "08001");}
}

        桥接模式的定义是“将抽象和实现解耦,让它们独立变化”。在jdbc这个例子中,JDBC本身相当于抽象,注意这里所说的抽象,指的并非是 抽象类 或 接口,而是跟具体的数据库无关的,被抽象出来的一套类库。
        具体的Driver(比如,com.mysql.cj.jdbc.Driver)就相当于 实现。注意 ,这里所说的实现,也并非指 接口的实现类 。而是跟具体数据库相关的一套 类库。JDBC  跟 Driver 独立开发,通过对象之间的组合关系,组装在一起。JDBC的所有逻辑操作,最终都委托给Driver来执行。

        这就是JDBC使用桥接模式的一个例子。通过将抽象的JDBC接口与具体的数据库厂商的实现相分离,我们可以实现对抽象和实现的独立扩展,这种设计模式使得在不修改客户端代码的情况下更换数据库驱动程序成为可能。从而提高了代码的可维护性和可扩展性。

         2、桥接模式原理解析

        桥接模式(Bridge Pattern)是一种结构型设计模式,用于将抽象与器实现相分离,以便两者可以独立地进行变化。它通过将抽象的类与实现的类分离,实现了抽象和实现的解耦。

        桥接模式的核心概念:
        1)抽象化:在抽象类中定义抽象业务的接口和一个对实现层次结构的引用。抽象化的主要目的是隐藏实现的细节,以便可以在不影响客户端的情况下更改实现。
        2)实现化:这是一个接口,定了实现抽象化的方法,不同的具体实现类可以有不同的实现方式。
        3)扩展抽象化:这是抽象化的一个具体实现,它定义抽象业务的具体操作。
        4)具体实现化:实现化接口的具体实现类,这些类为抽象业务提供具体的实现

        优点:
        1)抽线与实现分离,可以独立的进行变化
        2)提高了可扩展性
        3)符合单一职责原则,抽象部分专注于抽象化,实现部分专注于具体实现。

        缺点:
        1)增加了系统复杂性

        2.1 实现步骤

        首先定义具体实现化类的相关api,这里我们可以理解为具体的mysql的实现:

// 可以当做具体的jdbc实现
interface Implementor {void operationImpl();
}
// 具体实现化类A,具体实现可以多样化,独立变化
class ConcreteImplementorA implements Implementor {@Overridepublic void operationImpl() {System.out.println("具体实现A");}
}
// 具体实现化类B
class ConcreteImplementorB implements Implementor {@Overridepublic void operationImpl() {System.out.println("具体实现B");}
}

        接下来编写抽象化类的相关内容,可以理解为jdbc提供的api接口:

abstract class Abstraction {protected Implementor implementor;// 抽象依赖实现的接口,而不依赖实现本身public Abstraction(Implementor implementor) {this.implementor = implementor;}abstract void operation();
}
// 抽象化类
class RefinedAbstraction extends Abstraction {public RefinedAbstraction(Implementor implementor) {super(implementor);}// 请记住这里的方法名字不同,他们不需要实现相同的接口,他可以独立变化@Overridevoid operation() {implementor.operationImpl();}
}

        这里展示桥接的过程:

public class BridgePatternDemo {public static void main(String[] args) {Implementor implementorA = new ConcreteImplementorA();Abstraction abstractionA = new RefinedAbstraction(implementorA);abstractionA.operation();Implementor implementorB = new ConcreteImplementorB();Abstraction abstractionB = new RefinedAbstraction(implementorB);abstractionB.operation();}
}

        这个例子展示了桥接模式的核心概念,即将抽象与实现分离,这种分离使得我们可以独立的改变抽象类和实现类,提高了代码的可扩展性和可维护性。同时,这种设计也符合了单一职责原则,使得抽象部分和实现部分各自关注自己的核心职责。
        

        3、桥接模式的应用举例

        3.1 消息通知

        来看一个消息通知系统的例子,这系统需要支持多种通知方式(例如邮件、短信、即时消息等)以及多种通知紧急程度(普通、紧急、非常紧急等)。
        我们可以使用桥接模式将通知方式和通知紧急程度分离,使得它们可以独立的进行变化和扩展。

// 通知方式接口(实现化角色)
interface MessageSender {void send(String message);
}
// 邮件通知实现类
class EmailSender implements MessageSender {@Overridepublic void send(String message) {System.out.println("发送邮件通知: " + message);}
}
// 短信通知实现类
class SmsSender implements MessageSender {@Overridepublic void send(String message) {System.out.println("发送短信通知: " + message);}
}
// 即时消息通知实现类
class InstantMessageSender implements MessageSender {@Overridepublic void send(String message) {System.out.println("发送即时消息通知: " + message);}
}
// 抽象通知类(抽象化角色)
abstract class Notification {protected MessageSender messageSender;public Notification(MessageSender messageSender) {this.messageSender = messageSender;}abstract void notify(String message);
}
// 普通通知子类(扩展抽象化角色)
class NormalNotification extends Notification {public NormalNotification(MessageSender messageSender) {super(messageSender);}@Overridevoid notify(String message) {messageSender.send("普通:" + message);}
}
// 紧急通知子类(扩展抽象化角色)
class UrgentNotification extends Notification {public UrgentNotification(MessageSender messageSender) {super(messageSender);}@Overridevoid notify(String message) {messageSender.send("紧急:" + message);}
}
// 非常紧急通知子类(扩展抽象化角色)
class CriticalNotification extends Notification {public CriticalNotification(MessageSender messageSender) {super(messageSender);}@Overridevoid notify(String message) {messageSender.send("非常紧急:" + message);}
}
public class BridgePatternExample {public static void main(String[] args) {MessageSender emailSender = new EmailSender();MessageSender smsSender = new SmsSender();MessageSender instantMessageSender = new InstantMessageSender();Notification normalEmailNotification = newNormalNotification(emailSender);normalEmailNotification.notify("有一个新的任务待处理。");Notification urgentSmsNotification = new UrgentNotification(smsSender);urgentSmsNotification.notify("系统出现故障,请尽快处理!");Notification criticalInstantMessageNotification = newCriticalNotification(instantMessageSender);criticalInstantMessageNotification.notify("系统崩溃,请立即处理!");}
}

        这个示例中,我们使用桥接模式将通知方式(MessageSender接口以及其实现类)和通知紧急程度(Notification类及其子类)分离,这使得我们可以独立地添加更多的通知方法和通知紧急程度,而不会导致类的数量爆炸性增长。
        以下是如何运行这个示例的步骤:
        1)我们定义了一个MessageSender接口,用于表示通知方式,然后,我们创建了几个实现了MewssageSender接口的具体实现类:EmailSender、SmsSender和InstantMessageSender,它们分别表示通过邮件、短信和及时消息发送通知。
        2)然后,定义了一个抽象类Notification,它持有一个MessageSender对象,Notification类有一个抽象方法notify,用于发送通知。然后,我们创建了几个扩展Notification类的子类:NormalNotification、UrgentNotification 和 CriticalNotification ,它们分别表示普通、紧急 和 非常紧急的通知。
        3)在BridgePatternExample的main方法中,我们创建了 EmailSender
、 SmsSender 和 InstantMessageSender 对象,并将它们与不同紧急程度的通知对象组合,然后,我们调用这些通知对象的norify方法来发送通知。

        通过使用桥接模式,我们可以轻松的为消息通知系统添加新的通知方式和通知紧急程度,而无需修改现有的类结构,这种设计增强了代码的可扩展性和可维护性。

        3.2 支付方式

        看一电商项目中的例子,在这个例子中,需要处理多种支付方式(如信用卡、PayPal、支付宝等)和多种折扣策略(如VIP折扣,新用户折扣、优惠卷等)。

interface PaymentMethod {void pay(double amount);
}
// 信用卡支付实现类
class CreditCardPayment implements PaymentMethod {@Overridepublic void pay(double amount) {System.out.println("使用信用卡支付: " + amount);}
}
// PayPal支付实现类
class PayPalPayment implements PaymentMethod {@Overridepublic void pay(double amount) {System.out.println("使用PayPal支付: " + amount);}
}
// 支付宝支付实现类
class AlipayPayment implements PaymentMethod {@Overridepublic void pay(double amount) {System.out.println("使用支付宝支付: " + amount);}
}
// 折扣策略接口(抽象化角色)
abstract class DiscountStrategy {protected PaymentMethod paymentMethod;public DiscountStrategy(PaymentMethod paymentMethod) {this.paymentMethod = paymentMethod;}abstract double getDiscountedAmount(double originalAmount);public void payWithDiscount(double originalAmount) {double discountedAmount = getDiscountedAmount(originalAmount);paymentMethod.pay(discountedAmount);}
}
// VIP折扣策略子类(扩展抽象化角色)
class VipDiscountStrategy extends DiscountStrategy {public VipDiscountStrategy(PaymentMethod paymentMethod) {super(paymentMethod);}@Overridedouble getDiscountedAmount(double originalAmount) {return originalAmount * 0.9; // VIP用户享有9折优惠}
}
// 新用户折扣策略子类(扩展抽象化角色)
class NewUserDiscountStrategy extends DiscountStrategy {public NewUserDiscountStrategy(PaymentMethod paymentMethod) {super(paymentMethod);}@Overridedouble getDiscountedAmount(double originalAmount) {return originalAmount * 0.95; // 新用户享有95折优惠}
}
public class ECommerceExample {public static void main(String[] args) {PaymentMethod creditCardPayment = new CreditCardPayment();PaymentMethod payPalPayment = new PayPalPayment();PaymentMethod alipayPayment = new AlipayPayment();DiscountStrategy vipCreditCardStrategy = newVipDiscountStrategy(creditCardPayment);vipCreditCardStrategy.payWithDiscount(100);DiscountStrategy newUserPayPalStrategy = newNewUserDiscountStrategy(payPalPayment);newUserPayPalStrategy.payWithDiscount(100);}
}
        在这个示例中,我们使用桥接模式将支付方式( PaymentMethod 接口及其实现 类)和折扣策略( DiscountStrategy 类及其子类)分离 。这样,我们可以 独立地 添加更多的支付方式和折扣策略 ,而不会导致类的数量爆炸性增长。
        以下是如何运行这个示例的步骤:
        1. 我们定义了一个 PaymentMethod 接口,用于表示支付方式。然后,我们创建了几个实现了 PaymentMethod 接口的具体类: CreditCardPayment 、 PayPalPayment 和 AlipayPayment ,它们分别表示通过信用卡、 PayPal 和支 付宝进行支付。
        2. 接下来,我们定义了一个抽象类 DiscountStrategy ,它持有一个 PaymentMethod 对象。 DiscountStrategy 类有一个抽象方法 getDiscountedAmount ,用于计算折扣后的金额。然后,我们创建了几个扩 展 DiscountStrategy 类的子类: VipDiscountStrategy NewUserDiscountStrategy ,它们分别表示 VIP 折扣和新用户折扣。
        3. 在 ECommerceExample main 方法中,我们创建了 CreditCardPayment、 PayPalPayment 和 AlipayPayment 对象,并将它们与不同的折扣策略对象组合。然后,我们调用这些折扣策略对象的 payWithDiscount 方法来进行支付。 通过使用桥接模式,我们可以轻松地为电商项目添加新的支付方式和折扣策略,而无需修改现有的类结构。这种设计增强了代码的可扩展性和可维护性。
        

        

相关文章:

设计模式 三、结构型设计模式

一、代理模式 代理设计模式&#xff08;Proxy Design Pattern&#xff09;是一种结构型设计模式&#xff0c;它为其他对象提供了一个代理&#xff0c;以控制对这个对象的访问。 代理模式可以用于实现懒加载、安全访问控制、日志记录等功能。简单来说&#xff0c;代理模式 就是通…...

视频编码器的抉择:x264、x265、libaom、vvenc 对比测试实验

264、x265、libaom、vvenc 对比测试实验 测试机器配置&#xff1a;Apple M1 Pro -16G编码器版本&#xff08;选择自己编译&#xff09;&#xff1a;所有源码都是当前最新更新的状态&#xff0c;此外各类编码具体的编译过程可参考我的相关系列博客。 编码器GitHubx264git clon…...

JMeter脚本录制(火狐)

录制前准备&#xff1a; 电脑&#xff1a; 1、将JMeter证书导入&#xff0c;&#xff08;bin目录下有一个证书&#xff0c;需要安装这个证书到电脑中&#xff09; 2、按winr&#xff0c;输入certmgr.msc&#xff0c;打开证书&#xff0c;点击下一步&#xff0c;输入JMeter证书…...

10、Linux C 网络编程(完整版)

1、网络发展历史和分层 1.1 Internet 的历史 起源&#xff1a; 1957 年&#xff1a;苏联发射第一颗人造卫星 "Sputnik"。 1958 年&#xff1a;美国总统艾森豪威尔成立 DARPA&#xff08;国防部高级研究计划署&#xff09;。 1968 年&#xff1a;DARPA 提出 "…...

拼多多 anti-token unidbg 分析

声明: 本文章中所有内容仅供学习交流使用&#xff0c;不用于其他任何目的&#xff0c;抓包内容、敏感网址、数据接口等均已做脱敏处理&#xff0c;严禁用于商业用途和非法用途&#xff0c;否则由此产生的一切后果均与作者无关&#xff01; 逆向分析 版本7.3-7.4 都试过加密没什…...

Swoole 的 Hyperf 框架和 Go 的 Gin 框架高并发原理以及技术实现对比分析

Swoole 的 Hyperf 框架和 Go 的 Gin 框架虽然都支持高并发&#xff0c;但它们的实现原理、底层机制和适用场景有显著差异。以下从 高并发原理、技术实现区别、优缺点 三个方面详细分析&#xff1a; 一、高并发实现原理 1. Hyperf (PHP Swoole) Hyperf 的高并发能力基于 Swoo…...

CSS3学习教程,从入门到精通,CSS3 媒体查询实现响应式布局语法指南(21)

CSS3 媒体查询实现响应式布局语法指南 一、媒体查询核心语法 1. 基础语法结构 media 媒体类型 and (媒体特性) {/* 匹配条件时应用的CSS规则 */ }2. 媒体类型&#xff08;可省略&#xff09; 类型值说明all所有设备&#xff08;默认值&#xff09;screen屏幕设备print打印机…...

C#中,什么是委托,什么是事件及它们之间的关系

1. 委托&#xff08;Delegate&#xff09; 定义与作用 ‌委托‌是类型安全的函数指针&#xff0c;用于封装方法&#xff0c;支持多播&#xff08;链式调用&#xff09;。‌核心能力‌&#xff1a;将方法作为参数传递或异步回调。 使用场景 回调机制&#xff08;如异步操作完…...

【LeetCode 热题100】347:前 K 个高频元素(详细解析)(Go语言版)

&#x1f680; 力扣热题 347&#xff1a;前 K 个高频元素&#xff08;详细解析&#xff09; &#x1f4cc; 题目描述 力扣 347. 前 K 个高频元素 给你一个整数数组 nums 和一个整数 k&#xff0c;请你返回其中出现频率 前 k 高的元素。你可以按 任意顺序 返回答案。 &#x1f…...

②EtherCAT/Ethernet/IP/Profinet/ModbusTCP协议互转工业串口网关

型号 协议转换通信网关 EtherCAT 转 Modbus TCP 配置说明 网线连接电脑到模块上的 WEB 网页设置网口&#xff0c;电脑所连网口的网段设置成 192.168.1.X&#xff08;X 是除 8 外的任一数值&#xff09;后&#xff0c;打开浏览器&#xff0c;地址栏输入 192.168.1.8 &#xff…...

微服务集成测试 -华为OD机试真题(A卷、Python)

题目描述 现在有n个容器服务&#xff0c;服务的启动可能有一定的依赖性&#xff08;有些服务启动没有依赖&#xff09;&#xff0c;其次&#xff0c;服务自身启动加载会消耗一些时间。 给你一个n n 的二维矩阵useTime&#xff0c;其中useTime[i][i]10表示服务i自身启动加载需…...

k8s常用总结

1. Kubernetes 架构概览 主节点&#xff08;Master&#xff09;&#xff1a; 负责集群管理&#xff0c;包括 API Server、Controller Manager、Scheduler 和 etcd 存储。 工作节点&#xff08;Node&#xff09;&#xff1a; 运行 Pod 和容器&#xff0c;包含 kubelet、kube-pr…...

【算法】并查集基础讲解

一、定义 一种树型的数据结构&#xff0c;用于处理一些不相交集合的合并及查询问题。思想是用一个数组表示了整片森林&#xff08;parent&#xff09;&#xff0c;树的根节点唯一标识了一个集合&#xff0c;只要找到了某个元素的的树根&#xff0c;就能确定它在哪个集合里。 …...

探索PHP的未来发展与应用趋势

PHP&#xff0c;作为Web开发领域的常青树&#xff0c;自1995年诞生以来&#xff0c;始终在动态网页开发中占据重要席位。随着技术的不断演进&#xff0c;PHP也在持续更新&#xff0c;以适应现代开发需求。本文将深入探讨PHP的最新发展动态及其在2025年的应用趋势。 PHP 8&…...

C#调用ACCESS数据库,解决“Microsoft.ACE.OLEDB.12.0”未注册问题

C#调用ACCESS数据库&#xff0c;解决“Microsoft.ACE.OLEDB.12.0”未注册问题 解决方法&#xff1a; 1.将C#采用的平台从AnyCpu改成X64 2.将官网下载的“Microsoft Access 2010 数据库引擎可再发行程序包AccessDatabaseEngine_X64”文件解压 3.安装解压后的文件 点击下载安…...

ubuntu22.04.5安装docker,解决安装出现的错误,解决Docker hello-world没打印出来

文章目录 前言一 安装失败解决1结合具体报错分析2 首先怀疑是VPN的问题3 直接百度报错信息4最终解决问题 二 验证Docker hello-world没打印出来总结 前言 先说一下前面的情况&#xff0c;使用的是公司的工作站&#xff0c;登录公司一个帐号使用的公司网络&#xff0c;使用网上…...

HMTL+JS+CSS实现贪吃蛇游戏,包含有一般模式,困难模式,还有无敌模式

HMTLJSCSS实现贪吃蛇游戏&#xff0c;包含有一般模式&#xff0c;困难模式&#xff0c;还有无敌模式&#xff08;可以穿墙死不了&#xff0c;从左边进去可以从右边出来&#xff09;&#xff0c;显示当前分数和最高分&#xff0c;吃到的球颜色可以叠加到蛇身体上 为了适配手机端…...

vue将页面导出成word

方法一&#xff1a;使用 html-docx-js html-docx-js 是一个轻量级的库&#xff0c;可以将 HTML 转换为 Word 文档。 安装依赖 首先安装 html-docx-js&#xff1a; Bash深色版本 npm install html-docx-js --save创建导出逻辑 在 Vue 组件中实现导出功能的代码如下&#xff1…...

Spring MVC 页面跳转方案与区别

SpringMVC 的页面跳转方案主要分为 ‌转发&#xff08;Forward&#xff09;‌ 和 ‌重定向&#xff08;Redirect&#xff09;‌ 两类&#xff0c;具体实现方式和区别如下&#xff1a; 一、页面跳转方案 1. ‌转发&#xff08;Forward&#xff09;‌ 默认方式‌&#xff1a;直…...

Open GL ES ->纹理贴图,顶点坐标和纹理坐标组合到同一个顶点缓冲对象中进行解析

XML文件 <?xml version"1.0" encoding"utf-8"?> <com.example.myapplication.MyGLSurfaceView2 xmlns:android"http://schemas.android.com/apk/res/android"android:id"id/glSurfaceView"android:layout_width"matc…...

题解:AT_arc050_c [ARC050C] LCM 111

一道比较简单的题。&#xff08;我绝对不会告诉你这题我改了很久&#xff09; 题目意思很简单&#xff0c;我就不过多解释了&#xff0c;我们直接进入正题。 题目要我们求 a a a 个 1 1 1 组成的数与 b b b 个 1 1 1 组成的数的最小公倍数除以 m m m 后的余数。先不考虑…...

【408--考研复习笔记】计算机网络----知识点速览

目录 一、计算机网络体系结构 1.计算机网络的定义与功能&#xff1a; 2.网络体系结构相关概念&#xff1a; 3.OSI 七层模型与 TCP/IP 模型&#xff1a; 4.通信方式与交换技术&#xff1a; 电路交换 报文交换 分组交换 5.端到端通信和点到点通信&#xff1a; 6.计算机…...

ISIS报文

IS-IS 报文 目录 IS-IS 报文 一、报文类型与功能 二、报文结构解析 三、核心功能特性 四、典型应用场景 五、抓包数据分析 六、总结 IS-IS&#xff08;中间系统到中间系统&#xff09;协议报文是用于链路状态路由协议中网络设备间交换路由信息的关键载体&#xff0c;其设…...

FPGA——分秒计数器

文章目录 一、实验任务二、系统模块三、工程源码四、管脚信息五、运行结果参考资料总结 一、实验任务 在DE2-115板子上用 Verilog编程实现一个分秒计数器&#xff0c;并具备按键暂停、按键消抖功能。 二、系统模块 分频模块 高频时钟&#xff08;如50MHz&#xff09;分频得到…...

【Java】JVM

一、JVM体系结构 1、虚拟机概述 虚拟机&#xff08;Virtual Machine&#xff09;&#xff1a;一台虚拟的计算机&#xff0c;指一种特殊的软件&#xff0c;他可以在计算机平台和终端用户之间创建一种环境&#xff0c;而终端用户则是基于这个软件所创建的环境来操作软件。虚拟机…...

vue中使用geoscene无法出现弹窗

项目场景&#xff1a; 平日对地图加载使用不复杂的情况下&#xff0c;我通常采用leaflet去加载地图做一些简单的操作。但是最近需要用到arcgis发布的地图服务加载三维场景&#xff0c;于是又用回了geoscene&#xff08;arcgis国产化&#xff09;。这下暴露出很多的问题&#x…...

【Go】数组

数组Array 重点&#xff1a; 数组是值类型 注意点: 1. 数组&#xff1a;是同一种数据类型的固定长度的序列。2. 数组定义&#xff1a;var a [len]int&#xff0c;比如&#xff1a;var a [5]int&#xff0c;数组长度必须是常量&#xff0c;且是类型的组成部分。一旦定义&…...

【运维】Centos硬盘满导致开机时处于加载状态无法开机解决办法

Centos硬盘存储过满导致无法加载 一、准备1.现象2.根因分析3.制定救援方案问题1&#xff1a;无法进入系统确定分析结论 问题2&#xff1a;磁盘数据过多 4.后处理 一、准备 1.现象 Centos虚拟机界面卡顿&#xff0c;随后进行了重启操作&#xff0c;发现重新启动界面一直卡在加…...

JVM——模型分析、回收机制

方法区&#xff1a;存储已被虚拟机加载的类元数据信息(元空间) 堆&#xff1a;存放对象实例&#xff0c;几乎所有的对象实例都在这里分配内存 虚拟机栈&#xff1a;虚拟机栈描述的是|ava方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局…...

kafka 4.x docker启动kafka4.0.0 docker-compose启动最新版kafka 如何使用docker容器启动最新版kafka

1. 镜像选择标签&#xff1a; https://hub.docker.com/r/bitnami/kafka/tags 2. 命令&#xff1a; docker pull bitnami/kafka:4.0.0 3. docker-compose.yml 启动kafka4.0.0&#xff1a; version: 3services:kafka:image: bitnami/kafka:4.0.0container_name: kafkaports:- &…...

BUUCTF-web刷题篇(6)

15.PHP 知识点&#xff1a; ①__wakeup()//将在反序列化之后立即调用&#xff08;当反序列化时变量个数与实际不符是会绕过&#xff09;我们可以通过一个cve来绕过:CVE-2016-7124。将Object中表示数量的字段改成比实际字段大的值即可绕过wakeup函数。条件&#xff1a;PHP5<…...

MySQL篇(一):慢查询定位及索引、B树相关知识详解

MySQL篇&#xff08;一&#xff09;&#xff1a;慢查询定位及索引、B树相关知识详解 MySQL篇&#xff08;一&#xff09;&#xff1a;慢查询定位及索引、B树相关知识详解一、MySQL中慢查询的定位&#xff08;一&#xff09;慢查询日志的开启&#xff08;二&#xff09;慢查询日…...

QT之QML(简单示例)

需求一&#xff1a;点击按钮弹出菜单&#xff0c;并且自定义菜单弹出位置。 mouse.x 和 mouse.y 获取的是相对于 MouseArea&#xff08;在这个例子中是 Button&#xff09;左上角的局部坐标。如果你想要在鼠标点击位置显示 Menu&#xff0c;你需要将这个局部坐标转换为相对于应…...

自动化释放linux服务器内存脚本

脚本说明 使用Linux的Cron定时任务结合Shell脚本来实现自动化的内存释放。 脚本用到sync系统命令 sync的作用&#xff1a;sync 是一个 Linux 系统命令&#xff0c;用于将文件系统缓存中的数据强制写入磁盘。 在你执行reboot、poweroff、shutdown命令时&#xff0c;系统会默认执…...

Linux中的权限管理

一、权限的概念 在 Linux 系统的架构里&#xff0c;权限是构建安全堡垒的基石&#xff0c;精准界定了不同用户对文件与目录的操作边界&#xff0c;对系统安全的维护以及数据完整性的保障起着决定性作用。 1.权限的三种基础类别&#xff1a; 权限对文件的影响对目录的影响 读(r…...

Java对象与JSON字符串的互转

最近&#xff0c;工作中会涉及到Java对象与JSON字符串相互转换&#xff0c;虽然说并不难&#xff0c;但打算还是梳理一番&#xff0c;主要内容有&#xff1a; JSON 字符串 转 普通对象 普通对象 转 JSON 字符串 JSON 字符串数组 转 List 集合对象 List 集合对象 转 JSON 字符串…...

[笔记.AI]向量化

&#xff08;借助 DeepSeek-V3 辅助生成&#xff09; 向量化的定义 向量化&#xff08;Vectorization&#xff09; 是将文本、图像、音频等非结构化数据转换为高维数值向量&#xff08;即一组数字&#xff09;的过程。这些向量能够捕捉数据的语义、特征或上下文信息&#x…...

NSSCTF(MISC)—[justCTF 2020]pdf

相应的做题地址&#xff1a;https://www.nssctf.cn/problem/920 binwalk分离 解压文件2AE59A.zip mutool 得到一张图片 B5F31内容 B5FFD内容 转换成图片 justCTF{BytesAreNotRealWakeUpSheeple}...

Angular的理解

Angular 是一个由 Google 维护的全功能前端框架&#xff0c;适合构建复杂的企业级应用。它采用 TypeScript 作为首选语言&#xff0c;提供了一套完整的解决方案&#xff0c;包括数据绑定、依赖注入、路由、表单处理等。 1. Angular 的核心概念 1.1 组件化架构 Angular 应用由…...

广告推荐算法:COSMO算法与A9算法的对比

COSMO算法与A9算法的概念解析 1. A9算法 定义与背景&#xff1a; A9算法是亚马逊早期为电商平台研发的核心搜索算法&#xff0c;主要用于优化商品搜索结果的排序和推荐&#xff0c;其核心逻辑围绕产品属性与关键词匹配展开。自2003年推出以来&#xff0c;A9通过分析商品标题…...

10. 七大排序(含四种版本快排及优化) ******

排序算法时间复杂度(平均)时间复杂度(最坏)时间复杂度(最好)空间复杂度稳定性主要使用场景直接插入排序O(n)O(n)O(n)O(1)稳定小规模数据或基本有序数据希尔排序O(n^1.3)O(n)O(n log n)O(1)不稳定中等规模数据&#xff0c;对稳定性无要求选择排序O(n)O(n)O(n)O(1)不稳定小规模数…...

以下是C/C++后台开发常见的高概率面试题

一、语言基础 多态的实现 通过虚函数表&#xff08;vtable&#xff09;实现动态绑定&#xff0c;运行时根据对象类型调用对应的函数。虚函数通过virtual关键字声明&#xff0c;子类可重写基类虚函数112。 指针与引用的区别 指针是变量&#xff0c;存储地址&#xff0c;支持多…...

CentOS-查询实时报错日志-查询前1天业务报错gz压缩日志

最新版本更新 https://code.jiangjiesheng.cn/article/364?from=csdn 推荐 《高并发 & 微服务 & 性能调优实战案例100讲 源码下载》 1. 查询实时报错日志 物理路径(带*的放在靠后,或者不用*) cd /home/logs/java-gz-log-dir && tail -2000f java-gz-l…...

破界·共生:生成式人工智能(GAI)认证重构普通人的AI进化图谱

在当今这个科技日新月异的时代,人工智能(AI)正以惊人的速度改变着我们的世界。从智能家居到自动驾驶,从医疗诊断到金融分析,AI的应用已经渗透到社会生活的方方面面。面对如此迅猛的发展态势,我们不禁要问:人工智能的未来将走向何方?普通人又该如何把握这一历史机遇,学…...

HTTP代理:网页加速的隐形引擎

目录 引言&#xff1a;网页加载速度为何至关重要&#xff1f; 一、HTTP代理的核心加速原理 二、四大加速黑科技详解 三、实战场景性能对比 四、代理加速的隐藏代价 五、未来发展趋势 结语&#xff1a;智能代理的选型指南 引言&#xff1a;网页加载速度为何至关重要&#…...

Unity 常见报错 定位和查找方法

1.控制台 直接看报错信息 2.打log 例子&#xff1a; for(int i 0;i < 8;i) {Debug.Log(i);//这是打的log,看看到底i是几的时候出问题gameObject.name strs[i];} 3.断点调试 &#xff08;1&#xff09;在你想打断点的行&#xff0c;左边空白处点击可以打断点&#xff…...

人工智能之数学基础:初等反射阵

本文重点 在线性代数中,初等反射阵(Householder矩阵)作为一类特殊的正交矩阵,在矩阵变换、特征值计算及几何变换等领域具有广泛应用。其简洁的构造方式和丰富的数学性质,使其成为数值分析和几何处理中的重要工具。 什么是初等反射阵(豪斯霍尔德变换) I为单位矩阵,wwT…...

《Linux运维总结:基于银河麒麟V10操作系统+ARM64架构CPU二进制部署单机ACL版consul v1.18.1》

总结:整理不易,如果对你有帮助,可否点赞关注一下? 更多详细内容请参考:《Linux运维篇:Linux系统运维指南》 一、简介 1、什么是consul Consul是HashiCorp公司推出的开源工具,用于实现 分布式系统的服务发现与配置。 Consul是分布式的、高可用的、可横向扩展的。 架构图…...

web网站页面测试点---添加功能测试

添加 一、创建新的申请时&#xff0c;关闭网络查看数据是否存在&#xff0c;并提示网络错位相关提示语 二、在文本框内输入数据 1.在文本框内输入空格&#xff0c;查看文本内容前后是否存在空格 2.在文本框内输入最大长度&#xff0c;查看能否正确提交 3.在文本框内输入最大长…...

实操自动生成接口自动化测试用例

​这期抽出来的问题是关于如何使用Eolinker自动生成接口自动化测试用例&#xff0c;也就是将API文档变更同步到测试用例&#xff0c;下面是流程的示例解析。 导入并关联API文档和自动化测试用例 首先是登陆Eolinker&#xff0c;可以直接在线使用。 进入流程测试用例详情页&am…...