原文来自于公众号三不猴

基础概念

关于JNDI

Java命名和目录接口(Java Naming and Directory Interface,缩写JNDI),是Java的一个目录服务应用程序接口(API),它提供一个目录系统,并将服务名称与对象关联起来,从而使得开发人员在开发过程中可以使用名称来访问对象。

JNDI 与 Service Provider

JNDI 是统一抽象出来的接口,Service Provider是对接口的具体实现。如上面提到的默认的 JNDI Service Provider 有 RMI/LDAP 等等。

ObjectFactory

每一个 Service Provider 可能配有多个 Object Factory。Object Factory 用于将 Naming Service(如 RMI [Remote Method Invocation] / LDAP [Light Directory Access Portocol] )中存储的数据转换为 Java 中可表达的数据,如 Java 中的对象或 Java 中的基本数据类型。

开始手写

先启动一个本地RMI

public class MainTest {
    public static void main(String[] args) throws Exception {

        // 在本机 1999 端口开启 rmi registry,可以通过 JNDI API 来访问此 rmi registry
        Registry registry = LocateRegistry.createRegistry(1999);

        // 创建一个 Reference,第一个参数无所谓,第二个参数指定 Object Factory 的类名:

        // 第三个参数是 codebase,表明如果客户端在 classpath 里面找不到
        // jndiinj.EvilObjectFactory,则去 http://localhost:9999/ 下载

        // 当然利用的时候这里应该是一个真正的 codebase 的地址
        Reference ref = new Reference("test",
                "jndiinj.EvilObjectFactory", "http://localhost:9999/");

        // 因为只为只有实现 Remote 接口的对象才能绑定到 rmi registry 里面去
        ReferenceWrapper wrapper = new ReferenceWrapper(ref);
        registry.bind("evil", wrapper);
    }
}

    

连接本地客户端

public class LookupTest {
    public static void main(String[] args) throws NamingException {
        System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
        Context ctx = new InitialContext();
        // ctx.lookup 参数需要可控
        Object lookup = ctx.lookup("rmi://localhost:1999/evil");
        System.out.println(lookup);
    }
}

我们先启动MainTest类然后启动LookupTest类最后看看输出

  • Connected to the target VM, address: '127.0.0.1:49254', transport: 'socket' Reference Class Name: test

这里似乎好像没什么用,只是获取到了他们类名而已,能不能存个对象进去呢?

Reference中有个add方法

public void add(RefAddr addr) {
    addrs.addElement(addr);
}

这里添加的是一个RefAddr对象,RefAddr是一个抽象类,所以我们对这个拓展一下。

/**
 * @author yhx
 * @since 2021/9/10 5:33 下午
 */
public class BeanRef<T> extends RefAddr {
    private T t;

    public BeanRef(T t) {
        super(t.getClass().getName());
        this.t = t;
    }

    @Override
    public T getContent() {
        return t;
    }
}

我们对代码稍微修改一下,首先定义一个要存的对象,User类,这里去要注意的就是一定要实现序列化接口。

/**
 * @author yhx
 * @since 2021/9/10 3:21 下午
 */

public class User implements Serializable {

    private Long id;
    private String name;
    private String password;
    private String email;
    private String phone;
  // 省略set get方法,为了方便观察我们再定义一个String方法这里也省略
}

然后就是在创建完Reference对象以后加入下面的代码

 BeanRef<User> addr = new BeanRef<>(_getUser());
 ref.add(addr);
        
 private static User _getUser() {
        User user = new User();
        user.setId(0L);
        user.setName("小明");
        user.setPassword("***");
        user.setEmail("123@qq.com");
        user.setPhone("1821732098");
        return user;
    }       
        

重新执行LookupTest会得到下面的结果

Reference Class Name: user
Type: jndi.domain.User
Content: User[id=0, name='小明', password='***', email='123@qq.com', phone='1821732098']

这样我们就完成了一个依赖查找的小功能,可能你会说:就这???

其实Tomcat容器也提供了JNDI,这样我们也不用手动启动一个RMI。

首先我们先初始化一个web工程,不要引spring的依赖。在webapp下的META-INF新建context.xml文件,模板在Tomcat的example目录下有。

<?xml version="1.0" encoding="UTF-8"?>
<Context>
  <Resource 
      auth="Container" 
      driverClassName="com.mysql.jdbc.Driver" 
      maxIdle="30" 
      maxTotal="50" 
      maxWaitMillis="-1" 
      name="jdbc/MVNT1" 
      username="root"
      password="yhx" 
      type="javax.sql.DataSource" 
      url="jdbc:mysql://localhost:3306/learning?useSSL=true"/>
</Context>

然后我们在web.xml中新增下面的配置。注意名字要和context.xml中的名字一样。

<?xml version="1.0" encoding="UTF-8"?>
 <!DOCTYPE web-app PUBLIC
  '-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN'
  'http://java.sun.com/j2ee/dtds/web-app_2_3.dtd'>
<web-app> 
    <resource-ref>
          <description>DB Connection</description>
          <!-- 这个名字必须和数据源名字一致 -->
          <res-ref-name>jdbc/MVNT1</res-ref-name>
          <res-type>javax.sql.DataSource</res-type>
          <res-auth>Container</res-auth>
    </resource-ref>
</web-app>

获取数据源

数据源是获取和数据库的连接,所以一般我们可以创建 ServletContext监听器(implements ServletContextListener)在 ServletContext 创建的时候就去获取数据源。如下:

import javax.annotation.Resource;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;
import javax.sql.DataSource;

@WebListener
public class WebContextListener implements ServletContextListener {
    /*
     * 使用Resource注解成员变量,通过名字查找server.xml中配置的数据源并注入进来
     * lookup:指定目录处的名称,此属性是固定的
     * name:指定数据源的名称,即数据源处配置的name属性
     */
    @Resource(lookup="java:/comp/env", name="jdbc/MVNT1")

    /*将找到的数据源保存在此变量中,javax.sql.DataSource*/
    private DataSource dataSource;

    @Override
    public void contextDestroyed(ServletContextEvent event) {

    }

    @Override
    public void contextInitialized(ServletContextEvent event) {
        /*测试数据源*/
        //System.out.println(dataSource);

        /*将数据源注入连接管理*/
        ConnectionManager.setDadaSource(dataSource);
    }
}

然后再通过数据源获取连接

import java.sql.Connection;
import java.sql.SQLException;
import javax.sql.DataSource;

/*连接对象的管理者*/

public final class ConnectionManager {
    /*确保在每一个连接里是同一个连接对象,方便以后做事务的管理,针对每个线程创建一个独立的容器*/
    /*使用泛型标准*/
    private final static ThreadLocal<Connection> LOCAL=new ThreadLocal();
    private static DataSource dataSource;

    public static void setDadaSource(DataSource dataSource) {
        /*不能使用this*/
        ConnectionManager.dataSource=dataSource;
    }

    /*返回连接对象*/
    public static Connection getConnection() throws SQLException {
        /*获取连接对象*/
        Connection conn=LOCAL.get();
        if(null != conn) {
            return conn;
        }

        /*通过数据源得到连接,并放入线程中管理,再返回连接对象*/
        conn=dataSource.getConnection();
        LOCAL.set(conn);
        return conn;
    }

    /*释放连接对象*/
    public static void release() {
        Connection conn=LOCAL.get();
        if(null != conn) {
            DBUtil.release(conn);
            LOCAL.remove();
        }
    }
}

也可以直接使用Context来获取数据源(注意抛出异常)

Context cxt=new InitialContext();
//获取与逻辑名相关联的数据源对象
DataSource ds=(DataSource)cxt.lookup("java:comp/env/jdbc/MVNT1");

到此我们就把数据库DataSource对象使用jdni管理起来实现了一个依赖查找。

下篇我们将分析一下JNDI的实现原理。


三不猴
39 声望9 粉丝