java漫漫学习(七)

方法句柄

Java中的方法句柄(MethodHandle)是Java 7引入的一个强大的机制,用于在运行时动态调用方法。方法句柄与传统的反射API(如java.lang.reflect.Method)相比,具有更高的效率和更好的类型安全性。

方法句柄允许动态调用方法、构造函数和字段,类似于反射,但性能更高。例如,可以动态调用静态方法、实例方法或构造函数。

三个关键类:

  • Lookup:方法句柄的创建工厂

    // 创建一个只能对公共方法进行访问的lookup
    MethodHandles.Lookup publicLookup = MethodHandles.publicLookup();
    // 创建一个可对私有或保护方法进行访问的lookup
    MethodHandles.Lookup lookup = MethodHandles.lookup();
    
  • MethodType:方法的签名,包含方法的返回值类型,参数类型等信息

    public static MethodType methodType(Class<?> rtype, Class<?>[] ptypes)
    // 第一个参数为方法的返回类型,第二个参数为方法的参数类型
    // 因为java重载的特性,lookup需要根据方法的不同的签名来进行对应方法的查找,最终实现不同方法句柄的创建
    
  • MethodHandle:方法句柄,通过方法句柄进行动态的方法调用,访问对象的构造函数字段等。方法调用方式invokeinvokeWithArguments(无参数)、invokeExact(严格调用)。

通过LookupMethodType创建方法句柄的几种方式:

  • public MethodHandle findVirtual(Class<?> refc, String name, MethodType type)
    // 创建某个类的实例方法的方法句柄
    // 参数分别为:要查找的方法所在的类、要查找的方法的名称、要查找的方法的签名
    
  • public MethodHandle findStatic(Class<?> refc, String name, MethodType type)
    // 创建某个类的静态方法的方法句柄
    
  • public MethodHandle findConstructor(Class<?> refc, MethodType type)
    // 创建某个类的构造函数的方法句柄
    
  • public MethodHandle findGetter(Class<?> refc, String name, Class<?> type)
    // 创建某个类的非静态字段的getter方法句柄,这个方法句柄可用来动态的读取字段的值。
    
  • public MethodHandle findSetter(Class<?> refc, String name, Class<?> type)
    // 创建某个类的非静态字段的setter方法句柄,这个方法句柄可用来动态的设定字段的值
    

当你通过 MethodHandles.Lookup 创建一个方法句柄时,Java 会在创建时进行各种检查。这些检查包括访问权限检查(例如,确保你有权限访问私有方法或字段)、方法名称和类型匹配检查等。

如果创建方法句柄时遇到任何问题(例如,指定的方法不存在或你没有足够的访问权限),Java 会立即抛出异常。这意味着所有的验证工作都在查找方法句柄的过程中完成。

例:

// 创建 Lookup
MethodHandles.Lookup lookup = MethodHandles.lookup();
// 创建getRuntime方法的MethodType,getRuntime返回类型为Runtime.class,无参数
MethodType mt1 = MethodType.methodType(Runtime.class);
// 创建getRuntime方法的MethodHandle
MethodHandle getRuntime = lookup.findStatic(Runtime.class, "getRuntime", mt1);
// 创建exec方法的MethodType,exec返回类型为Process.class,参数类型为String.class
MethodType mt2 = MethodType.methodType(Process.class, String.class);
// // 创建exec方法的MethodHandle
MethodHandle exec = lookup.findVirtual(Runtime.class, "exec", mt2);
// 调用getRuntime方法获取Runtime对象
Object runTime = getRuntime.invokeWithArguments();
// 调用Runtime对象的exec函数
exec.invoke(runTime, "calc");

SPI

SPI(Service Provider Interface)是一种提供服务发现和动态加载机制的API。它允许模块化程序在运行时发现和使用服务实现,而无需在编译时绑定具体的实现类。SPI Java 平台的一个核心机制,特别在构建可插拔的、灵活的系统时非常有用。

API中,API实现方制定接口并完成对接口的实现,调用方仅仅依赖接口调用,无权选择不同实现。API更多的是被应用开发人员使用。

SPI中,应用方制定接口规范,提供给外部来实现,调用方在调用时则选择自己需要的外部实现。SPI更多的被框架扩展人员使用。

工作原理

  • 1、定义服务接口:创建一个接口或抽象类,定义服务的契约。这是所有Service Provider必须实现的接口。

    public interface PaymentProcessor {
        void processPayment(double amount);
    }
    
  • 2、Service Provider实现:实现服务接口的一个或多个具体类,这些类称为Service Provider。用户可以根据自己的需要选择某个或者多个Service Provider来调用。

    public class PaypalProcessor implements PaymentProcessor {
        @Override
        public void processPayment(double amount) {
            System.out.println("Processing payment of $" + amount + " via PayPal.");
        }
    }
    
    public class CreditCardProcessor implements PaymentProcessor {
        @Override
        public void processPayment(double amount) {
            System.out.println("Processing payment of $" + amount + " via Credit Card.");
        }
    }
    
  • 3、服务描述文件:在 META-INF/services 目录下创建一个文件,文件名是服务接口的全限定名,文件内容是Service Provider实现类的全限定名。

    // 文件名:PaymentProcessor
    com.example.PaypalProcessor
    com.example.CreditCardProcessor
    
  • 4、加载服务:使用 java.util.ServiceLoader 类动态加载并发现Service Provider

    import java.util.ServiceLoader;
    
    public class PaymentService {
        public static void main(String[] args) {
            // 查找和加载 PaymentProcessor 的实现类
            ServiceLoader<PaymentProcessor> loader = ServiceLoader.load(PaymentProcessor.class);
    		// 遍历并使用所有找到的 PaymentProcessor 实现
            for (PaymentProcessor processor : loader) {
                processor.processPayment(100.0);
            }
        }
    }
    

JDBC

Java JDBC(Java Database Connectivity)是 Java 平台的一个 API用于连接和操作关系数据库JDBC 提供了一组标准的接口和类,使开发者可以通过这些接口和类以统一的方式与各种数据库进行交互,而不需要关心底层数据库的具体实现。

JDBC 包含以下几个主要组件:

  1. Driver Manager(驱动管理器)
    • 管理一组数据库驱动程序。应用程序可以通过 DriverManager 类来加载驱动程序,并使用它们来连接数据库。
    • 常用方法:DriverManager.getConnection(String url, String user, String password)
  2. Connection(连接)
    • 表示与数据库的连接。通过 Connection 对象可以创建语句对象,提交和回滚事务,获取元数据等。
    • 常用方法:createStatement(), prepareStatement(String sql), commit(), rollback()
  3. Statement(语句)
    • 用于执行静态 SQL 语句并返回结果。包括 Statement, PreparedStatement, CallableStatement
    • 常用方法:executeQuery(String sql), executeUpdate(String sql), execute(String sql)
  4. ResultSet(结果集)
    • 表示数据库查询的结果。通过 ResultSet 对象可以遍历查询结果,并获取每一行的数据。
    • 常用方法:next(), getString(int columnIndex), getInt(String columnLabel)
  5. SQLExceptionSQL 异常)
    • 处理与数据库操作相关的异常。所有与数据库相关的错误都通过 SQLException 抛出。

JDBC连接数据库,进行查询与结果处理示例:

1、加载数据库驱动程序

try {
    Class.forName("com.mysql.cj.jdbc.Driver");
} catch (ClassNotFoundException e) {
    e.printStackTrace();
}

驱动jar包下的META-INF/services文件夹中的java.sql.Driver文件(文件名为SPI接口的全限定名)中已经把Driver类路径记录下来了,若程序中没有注册驱动,会先读取这个文件,自动注册驱动。这里就用到了SPI技术,它通过在ClassPath路径下的META-INF/services文件夹查找文件,实现类自动加载

2、建立数据库连接

String url = "jdbc:mysql://localhost:3306/mydatabase";
String user = "username";
String password = "password";
Connection conn = null;

try {
    conn = DriverManager.getConnection(url, user, password);
} catch (SQLException e) {
    e.printStackTrace();
}

3、创建语句并执行查询

Statement stmt = null;
ResultSet rs = null;

try {
    stmt = conn.createStatement();
    rs = stmt.executeQuery("SELECT * FROM mytable");

    while (rs.next()) {
        String column1 = rs.getString("column1");
        int column2 = rs.getInt("column2");
        System.out.println("Column1: " + column1 + ", Column2: " + column2);
    }
} catch (SQLException e) {
    e.printStackTrace();
}

4、关闭连接

finally {
    try {
        if (rs != null) rs.close();
        if (stmt != null) stmt.close();
        if (conn != null) conn.close();
    } catch (SQLException e) {
        e.printStackTrace();
    }
}

前面指出,JDBC建立数据库驱动时,使用了SPI技术,通过在ClassPath路径下的META-INF/services文件夹查找文件,实现类自动加载。因此我们可以自己编写恶意jar包,并让用户引入这个恶意jar包,jarMETA-INF/services文件夹中的java.sql.Driver文件内容为我们自己编写的恶意类(java.sql.Driver的子类)的路径,这样在用户执行JDBC连接数据库操作的时候就可以进行恶意命令执行。

JNDI

Java Naming and Directory InterfaceJava命名与目录接口,是 Java 平台的一部分,它提供了命名和目录服务的APIJNDI 允许 Java 应用程序通过一个统一的接口来访问不同的命名和目录服务,使Java应用程序能和这些命名服务和目录服务之间进行交互。

目录服务是命名服务的一种自然扩展:

  • Naming Service:命名服务是将一个服务名称与对象或命名引用相关联,也称为"绑定"。它允许应用程序在运行时查找对象,而不需要在编译时知道对象的具体位置。在一些命名服务系统中并不直接将对象进行存储,而是存储了对象的引用,引用包含了如何访问实际对象的信息,类似于指针。例如RMI Registry就是使用的Naming Service
  • Directory Service:是一种特殊的Naming Service,它允许存储和搜索"目录对象"。一个目录对象不同于一个通用对象,目录对象可以与属性关联,例如创建时间、读写执行权限等。因此,目录服务提供了根据对象属性来进行操作的功能扩展。不仅可以根据名称去查找(lookup)对象(并获取其对应属性),还可以根据属性值去搜索(search)对象。一个目录是由相关联的目录对象组成的系统,一个目录类似于数据库,不过它们通常以类似树的分层结构进行组织。例如LDAPRMI就是目录服务。

JNDI 独立于具体的目录服务实现,因此可以针对不同的目录服务提供统一的操作接口JNDI 架构上主要包含两个部分,即 Java 的应用层接口和 SPI,如下图所示:

在这里插入图片描述

SPI 即服务供应接口,为底层的具体目录服务提供了统一接口,从而实现目录服务的可插拔式安装。由于 SPI 的统一接口,RMILDAP等目录服务实现了可插拔式安装。厂商也可以提供自己的私有目录服务实现,用户可无需重复修改代码,就和安装其他的目录服务也就是服务提供者(Service Provider),这些目录服务本身和 JNDI 没有直接耦合性,但基于 SPI 接口和 JNDI 构建起了重要的联系。

Java JDK里面提供了5个包,提供给JNDI的功能实现,分别是:

  • javax.naming:主要用于命名操作,包含了访问目录服务所需的类和接口,比如 ContextBindingsReferenceslookup 等。
  • javax.naming.directory:主要用于目录操作,它定义了DirContext接口和InitialDir- Context类;
  • javax.naming.event:在命名目录服务器中请求事件通知;
  • javax.naming.ldap:提供LDAP支持;
  • javax.naming.spi:允许动态插入不同实现,为不同命名目录服务供应商的开发人员提供开发和实现的途径,以便应用程序通过JNDI可以访问相关服务。

JNDI核心组件:

  • **Context **:ContextJNDI 的核心,表示上下文。一个上下文中对应着一组名称到对象的绑定关系,我们可以在指定上下文中查找一个名称对应的对象。比如在文件系统中,一个目录就是一个上下文,可以在该目录中查找文件,其中子目录也可以称为子上下文 (subcontext)。它提供了一组方法来绑定、重新绑定、查找、以及删除对象。

    javax.naming.InitialContext类:获取初始目录环境

    //构建一个初始上下文。
    InitialContext() 
    //构造一个初始上下文,并选择不初始化它。
    InitialContext(boolean lazy) 
    //使用提供的环境构建初始上下文。
    InitialContext(Hashtable<?,?> environment)  
    
  • BindingsBindings 表示一个名称和对象之间的绑定关系。比如在文件系统中文件名绑定到对应的文件。

    InitialContext类常用方法:

    //将名称绑定到对象。 
    bind(Name name, Object obj) 
    //枚举在命名上下文中绑定的名称以及绑定到它们的对象的类名。
    list(String name) 
    //检索命名对象。
    lookup(String name)  
    //将名称绑定到对象,覆盖任何现有绑定。
    rebind(String name, Object obj) 
    //取消绑定命名对象。
    unbind(String name) 
    
  • Reference:一个特殊的对象,用于存储一个命名系统中对象的引用。Reference 包含足够的信息来查找或重新创建对象。比如文件系统中实际根据名称打开的文件是一个整数 fd ,这就是一个引用,内核根据这个引用值去找到磁盘中的对应位置和读写偏移。

    javax.naming.Reference类:表示对在命名/目录系统外部找到的对象的引用。提供了JNDI中类的引用功能。

    //为类名为“className”的对象构造一个新的引用。
    Reference(String className) 
    //为类名为“className”的对象和地址构造一个新引用。 
    Reference(String className, RefAddr addr) 
    //为类名为“className”的对象,对象工厂的类名和位置以及对象的地址构造一个新引用。 
    Reference(String className, RefAddr addr, String factory, String factoryLocation) 
    //为类名为“className”的对象以及对象工厂的类名和位置构造一个新引用。  
    Reference(String className, String factory, String factoryLocation)
    /*
    参数:
    className 远程加载时所使用的类名
    factory  加载的class中需要实例化类的名称
    factoryLocation  提供classes数据的地址可以是file/ftp/http协议
    */
    

    例:

    import com.sun.jndi.rmi.registry.ReferenceWrapper;
    import javax.naming.NamingException;
    import javax.naming.Reference;
    import java.rmi.AlreadyBoundException;
    import java.rmi.RemoteException;
    import java.rmi.registry.LocateRegistry;
    import java.rmi.registry.Registry;
    
    public class jndi {
        public static void main(String[] args) throws NamingException, RemoteException, AlreadyBoundException {
            String url = "http://127.0.0.1:8080"; 
            Registry registry = LocateRegistry.createRegistry(1099);
            Reference reference = new Reference("test", "test", url);
            // 注册到RMI Registry的对象需要实现Remote接口并且继承UnicastRemoteObject,但是Reference并没有实现,因此需要ReferenceWrapper进行包装
            ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
            registry.bind("aa",referenceWrapper);
        }
    }
    

LDAP

Lightweight Directory Access Protocol:轻量目录访问协议,运行在**TCP/IP堆栈之上。LDAP目录服务是由目录数据库和一套访问协议组成的系统。目录服务是一个特殊的数据库,用来保存描述性的、基于属性的详细信息,能进行查询、浏览和搜索,以树状结构组织数据。LDAP目录服务基于客户端-服务器模型**,它的功能用于对一个存在目录数据库的访问。 LDAP协议主要用于单点登录SSO(Single Sign on)(用户只需登录一次,就可以访问所有与该用户账户关联的系统和服务,而不需要为每个系统单独进行身份验证)。

DNDistinguished Name,区分名)是 LDAP 中用来唯一标识一个目录条目的字符串。它表示条目在目录信息树中的位置。DN 是由一系列相互连接的 RDNRelative Distinguished Name,相关区分名)组成的,每个 RDN 代表目录树中的一个节点。

一些RDN如下:

  • DCDomain Component,域组件):用于将 DNS 域名表示为 LDAP 目录的一部分。多个 DC 组件可以组合来表示完整的域名。
  • OUOrganizational Unit,组织单位):一种逻辑分组,用于将条目分类到目录树中的特定部分。OU 可以包含其他 OU 或条目。
  • UIDUser ID,用户标识):用于唯一标识目录中的用户条目。UID 通常包含用户的唯一用户名或 ID

例:使用 JNDI 连接到 LDAP 服务器,并进行增删改查:

import javax.naming.Context;
import javax.naming.NamingEnumeration;
import javax.naming.directory.*;
import java.util.Hashtable;

public class LdapExample {
    // 方法:连接到 LDAP 服务器
    public static DirContext connect(String url, String user, String password) throws Exception {
        Hashtable<String, String> env = new Hashtable<>();
        env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
        env.put(Context.PROVIDER_URL, url);
        env.put(Context.SECURITY_AUTHENTICATION, "simple");
        env.put(Context.SECURITY_PRINCIPAL, user);
        env.put(Context.SECURITY_CREDENTIALS, password);
		// 使用提供的环境构建初始上下文
        return new InitialDirContext(env);
    }

    // 方法:添加条目
    public static void addEntry(DirContext ctx) throws Exception {
        // 创建条目属性
        Attributes attrs = new BasicAttributes();
        Attribute objClass = new BasicAttribute("objectClass");
        objClass.add("inetOrgPerson");
        attrs.put(objClass);
        attrs.put("cn", "John Doe");
        attrs.put("sn", "Doe");
        attrs.put("givenName", "John");
        attrs.put("mail", "johndoe@example.com");
        attrs.put("uid", "johndoe");

        // DN,在 example.com 域的 Users 组织单位中的用户 johndoe
        String dn = "uid=johndoe,ou=Users,dc=example,dc=com";

        // 添加条目
        ctx.createSubcontext(dn, attrs);
        System.out.println("Entry added: " + dn);
    }

    // 方法:搜索条目
    public static void searchEntry(DirContext ctx) throws Exception {
        String searchBase = "ou=Users,dc=example,dc=com";
        String searchFilter = "(uid=johndoe)";

        SearchControls searchControls = new SearchControls();
        searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);

        NamingEnumeration<SearchResult> results = ctx.search(searchBase, searchFilter, searchControls);

        while (results.hasMore()) {
            SearchResult result = results.next();
            Attributes attrs = result.getAttributes();
            System.out.println("Found entry: " + result.getNameInNamespace());
            System.out.println("Attributes: " + attrs);
        }
    }

    // 方法:删除条目
    public static void deleteEntry(DirContext ctx) throws Exception {
        String dn = "uid=johndoe,ou=Users,dc=example,dc=com";

        // 删除条目
        ctx.destroySubcontext(dn);
        System.out.println("Entry deleted: " + dn);
    }
    public static void main(String[] args) {
        try {
            // 连接到 LDAP 服务器
            DirContext ctx = connect("ldap://localhost:389", "cn=admin,dc=example,dc=com", "password");
            System.out.println("Connected to LDAP server.");
            // 添加条目
            addEntry(ctx);
            // 搜索条目
            searchEntry(ctx);
            // 删除条目
            deleteEntry(ctx);
            // 关闭连接
            ctx.close();
            System.out.println("Disconnected from LDAP server.");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值