缘起
公司项目需要使用LDAP服务, 遂研究了2天. 昨天下午想写个LDAP分页, 百度万千, 结果都是抄来抄去的不可用的代码. 感觉博客的作者自己都没弄懂Spring LDAP的API分类就瞎抄一气. 我极为光火. 遂花了一晚上阅读了Spring LDAP 2.3.2 的官方文档. 豁然开朗, 并且写了基于最新的spring-ldap2.3.2分页的demo. 关于参考文档,见【1-3】
本文并不想仔细分析其中的API,其实这一点直接去看官文就很容易看懂了. 英文不好的童鞋直接看中文翻译的1-5章. 秒懂. 比百度搜的抄来抄去作者都不知所谓的博文好上百倍.
分析
Spring LDAP 封装了LDAP原生的JNDI操作. Spring LDAP 的API 分成2类
- 实体类不带注解的
- 实体类带注解的
不带注解的需要自己写Mapper(不论是AttributesMapper还是较为方便的ContextMapper), 而为了解决需要额外定义Mapper的代码繁琐问题,Spring LDAP 引入了ODM(Object Directory Mapping)注解. 该注解就是提供了实体类属性和LDAP目录结构之间的映射,这和JPA注解是类似的. 本质上就是实现了Mapper的功能. 所以不难知道, 后者在增删改查的时候是不需要传入Mapper或者有类似于做Mapper的事情的举动的. 这里只说两个问题
- 在使用ODM注解的时候, 具体遇到的问题和自己的理解
- 分页的正确姿势
可能下面的写的比较乱,但是绝对是使用spring ldap 2.3.2的干货. 因为下面要讲的一些东西都是我demo中试出来的, 用语言不好表达,所以很多博客或者官文干脆寥寥数言就带过 但是如果理解不到位的话, 就很难真正把Spring LDAP的API用好的. 所以下面的语言会比较啰嗦.
使用ODM注解时遇到的问题
这里首先说一句,ODM注解虽然代码简单,但是操作并没有不带注解的简明直接. 所以其实我更喜欢用不带注解的方式.
Q: 注解@Entry的属性objectClasses意味着什么?
A: 意味着如果查到的条目中没有包含这里写的objectClasses全部值的话, 就不会被查询出. 相当于过滤. 当然, 官网上也有解释.
Q: 注解@Entry的属性base意味着什么?
A: 这个必须要和另一个注解 @DnAttribute 结合在一起讲.
项目的DIT树基本结构如下
@DnAttribute 有2个属性,一个叫value,一个叫index. 写了这个注解的属性是一定要参与条目dn值的构建的,所以不能为null, 不然创建条目的时候报错. value表示它在dn中的名称. index就是它的索引位置(从@Entry的base属性开始,第一个index为0). 这样讲依旧好抽象,我们来举个栗子吧!
比如(具体代码参见DEMO【1】)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16"o=xxx,ou=study", objectClasses = { "inetOrgPerson" }) (base =
public class Person {
"cn", index = 2) (value =
// @Attribute(name = "cn")
private String ouu;
"cn", index = 1) (value =
// @Attribute(name = "cn")
private String ouu2;
"ou", index = 0) (value =
"ou") (name =
// @Transient // 注意,这是spring ldap 包下的Transitent, 不要写错了!!!不然看不到效果的
private String ouu1;
...则你在创建对象的时候, 即
1
ldapTemplate.create(person);
的时候, 会读取person入参的ouu(比如说study5)、ouu1(比如说vvv)、ouu2(比如说cfs)的值,然后拼成该条目的dn值
1
cn=study5,cn=cfs,ou=vvv,o=xxx,ou=study,o=myorg,dc=yfs,dc=com
注意,上面的dn值其实分成了3部分
- cn=study5,cn=cfs,ou=vvv, 这就是 上面三个@DnAttribute属性. ouu2映射成了0号位置的ou, 其实就是从右到左, 截掉下面的2和3开始从零算起.
- o=xxx,ou=study 这其实就是@Entry注解中的base值
- o=myorg,dc=yfs.dc=com 这是整个ldap连接的base dn,简记P, 在配置文件中写的. 注意,一旦这个P配置好了,则API中所有的操作要写的dn入参都是相对于P来写的, 不要再拼上P,不然报NameNotFound.
所以就知道了为什么@DnAttribute注解的属性不能为null了. 不然的话, 无法构建dn值,那ldap怎么知道你要把这个条目放在ldap的哪个位置呢? 这里多说一句,属性的属性名是不能乱写的(不能写ouu、ouu1、ouu2,不然会报type undefined的, 所以才要使用@Attribute进行映射), 例如上面的ou、cn,因为这些都是在ldap服务器的scheme约束文件中写的. 可以自定义自己的属性名,甚至objectclass.
而且要注意, 你上传一个cn=study5,cn=cfs,ou=vvv,o=xxx,ou=study,o=myorg,dc=yfs,dc=com这样的条目的话, 则必须要保证中间的条目,例如
cn=cfs,ou=vvv,o=xxx,ou=study,o=myorg,dc=yfs,dc=com
ou=vvv,o=xxx,ou=study,o=myorg,dc=yfs,dc=com
o=xxx,ou=study,o=myorg,dc=yfs,dc=com
ou=study,o=myorg,dc=yfs,dc=com
o=myorg,dc=yfs,dc=com
都已近建立好了,不然报NameNotFoud. 这其实并不难理解的, 因为条目中有很多属性,你不可能就给ldap服务器一个子条目,就让ldap服务器猜测所有父条目的属性一路把父条目给你自动创建出来.
还要说一句,@Entry注解中的base属性并不意味着查询节点的时候从”base拼上ldap连接配置的base dn”这个地址搜索起, base和搜索的起点完全是不搭嘎的两个东西. base只是用来计算创建的条目的dn值的. 而且必须要有@DnAttribute 注解的属性, 不然报 Unable to determine id for entry com.yfs.po.Person异常. 而LdapTemplate中的find方法如不指定搜索起点(即base入参),则都是从ldap连接中的base dn开始搜起. 和@Entry中的base完全无关.
其实很正常, 因为LDAP只有一个根条目,你总会在根条目下面创建子条目啊.
例如我们可以
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15"inetOrgPerson" }) ( objectClasses = {
public class Person {
// @JsonIgnore // @JsonIgnore是为了将person传给前端时不报错,因为Name类型的无法自动解析成json格式。但是这里没导入json的包
private Name dn;
"ou", index = 0) (value =
// @Attribute(name = "ou")
// 注意,这是spring ldap 包下的Transitent, 不要写错了!!!不然看不到效果的
private String ouu1;
"cn") (name =
private String commonName;
"sn") (name =
private String suerName;注意,我们没有填写注解 @Entry 的base属性, 然后
1
2
3
4
5
6
7
8
public void test1() {
Person person = new Person();
person.setCommonName("xin");
person.setSuerName("xin");
person.setOuu1("vvv");
personRepo.create(person);
}则就可以
我们就在根条目下面创建了vvv条目. 所以@DnAttribute 属性的好处是显然的. 那就是可以在创建和更新条目的时候spring-ldap框架自动计算条目的dn值,则就知道这个条目在DIT中的位置. 特别是更新,就可以更新完毕一update则条目自动移位置了.
最后我们来讲讲上面出现的@Transient注解. 首先这个注解是
1
org.springframework.ldap.odm.annotations.Transient;
即ldap包下的, 还有一个
1
org.springframework.data.annotation.Transient
不要导包的时候导错了,导致最后看不到@Transient的效果. 被这个注解注解的属性将不会成为条目的属性. 首先,比较好理解的@Attribute注解的含义大家都知道(N多博客总算不至于把这个都写错). 就是被注解的属性创建条目以及读取条目的时候,条目中的属性自动映射到成员变量. 但是学过JPA的童鞋都知道,JPA也有@Transient注解, 一旦被注解就不再会被存储到数据库中,举一反三,被此注解的成员变量就不会成为条目中的属性. 也就是创建条目的时候,此属性不会成为条目中的属性. 读取的时候自然没有被读取条目中的属性映射到此属性. 譬如一个同时被@DnAttribute 和 @Transient 注解的属性的作用就仅仅是参与dn的构建(因此不会为null),但是不会参与条目的属性的事情. 但是也有例外,例外就是条目的rdn值一定会参与属性的构建,而不论它有没有被@Transient注解,干说太抽象. 下面举一个栗子.
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"o=xxx,ou=study", objectClasses = { "inetOrgPerson" }) (base =
public class Person {
// @JsonIgnore // @JsonIgnore是为了将person传给前端时不报错,因为Name类型的无法自动解析成json格式。但是这里没导入json的包
private Name dn;
"cn", index = 2) (value =
// @Attribute(name = "cn")
private String ouu;
"cn", index = 1) (value =
// @Attribute(name = "cn")
private String ouu2;
"ou", index = 0) (value =
"ou") (name =
// @Transient // 注意,这是spring ldap 包下的Transitent, 不要写错了!!!不然看不到效果的
private String ouu1;
"cn") (name =
private String commonName;
"sn") (name =
private String suerName;
... 省略 gettor、settor
}1
2
3
4
5
6
7
8
9
10
public void test1() {
Person person = new Person();
person.setCommonName("xin");
person.setSuerName("xin");
person.setOuu("study6");
person.setOuu1("vvv");
person.setOuu2("cfs");
personRepo.create(person);
}前提前面说了,需要想创建的子条目的所有父条目都已经创建好了,不然报NameNodeNotFound。则最后效果是
可以清晰的看到成功创建了study6条目 有ou=vvv属性. 这是因为 ouu1 被映射为ou,并且没有被 @Transient注解,所以自动成为了条目属性,而ouu2被cn,ouu2为cfs,但是条目属性中并没有cn=yfs属性. 这是因为ouu2成员上被注解了@Transient. 最后虽然ouu被映射为了cn,值为study6,而且也打上了@Transient注解,但是因为study6是rdn(也就是dn最左边的值),所以尽管你注解了@Transient, 依旧会成为属性的.
如果打开第八行注释,注掉第九行的话, 结果不变. 如果打开13行注释,注掉第14行的话,则变成
可见,ouu2(值为cfs)也变成了新建条目的属性.
如果继续注掉17行,打开18行注释的话,则
可以发现 ou=vvv 属性已经不见了. 因为他被transient掉了.
分页的正确姿势
分页网上基本抄来抄去就是那段代码,而且注释都一样抄. 就不能自己理解一下吗? 官网上给了现成的demo啊!
具体代码参见DEMO【1】中的searhByPage方法,这里讲一下我对他的理解. LDAP分页其实就是我要找第5页,每页3条数据的结果集. 则ldap很笨的,会从第一页开始给你找,然后每找到一页就会执行你传入的回调, 我们只需要在一页一页找的过程中加入一个计数器,到了第5页就终止查找(如果要找的页数超过最大页数的话, 也只返回最后一页的数据),然后返回即可. 这就是分页的原理.
参考
【1】https://docs.spring.io/spring-ldap/docs/current/reference/ Spring LDAP 2.3.2 官方文档
【2】https://www.jianshu.com/p/77517e26a357 Spring LDAP 2.3.2 官方文档 1-5 章翻译
【3】https://www.jianshu.com/p/835c2db4a1c4 Spring LDAP 2.3.2 官方文档 6-10 章(不完整)翻译
DEMO
【1】https://github.com/yfsyfs/backend/tree/master/springboot-ldap
v1.5.2