HR人員和組織信息同步AD域服務器
- 前期準備
- AD域基礎知識整理
- HR同步AD的邏輯
- 代碼結構
- 配置文件設置
- 啟動類
- HR組織的Bean
- HR人員Bean
- 獲取HR人員和組織信息的類
- AD中處理組織和人員的類
- 日志配置
- POM.xml文件
- 生成EXE文件
- 服務器定時任務
- 異常問題注意事項
前期準備
1、開發語言:Java
2、開發框架:無
3、日志框架:logback
4、服務器:windows2016(已部署了AD域,這里不過多介紹)
5、開發工具:idea、launch4j(用于部署服務器定時任務生成exe用)
6、AD域證書(客戶端連接AD使用)
AD域基礎知識整理
AD域服務器中重要的知識點或屬性描述:
- “CN”(Common Name,常用名),用于指定對象的具體名稱
- “DC”(Domain Component,域組件),用來標識域的各個部分
- “Description”,可對對象進行詳細的描述說明,在這里存放的為組織編碼
- “adminDescription”,用于存放組織id
- “DistinguishedName”(可分辨名稱),是 OU 在 AD 中的唯一標識,它描述了 OU 在域中的完整路徑
- “mobile”,用于記錄用戶的手機號
- “department”,用于記錄部門的ID
- “displayName”,用于記錄用戶的顯示名稱
- “info”,用于記錄用戶的ID
- “sn”,用于記錄用戶的姓
- “givenName”,用于記錄用戶的名
- “unicodePwd”,用于記錄用戶的密碼,賦值時用十六進制
- “userAccountControl”,用于控制用戶狀態,正常賬戶為514,禁用賬戶為514
- “pwdLastSet”,用于控制用戶下次登陸時是否需要更改密碼
HR同步AD的邏輯
1、數據準備:將HR中的組織和人員信息建立一個Bean方法
2、連接與認證:
①連接HR系統,可以通過接口,也可通過導入外部jar包的方式(此文章用導入外部jar包的方式獲取HR中的信息)
②建立AD的系統連接
3、根據HR的信息處理AD中的信息,先處理組織,再處理人員
4、記錄日志并打印
代碼結構
配置文件設置
記錄AD和HR系統的各種信息
public class AppConfig {// SHR Configurationpublic static final String SHR_URL = "HR系統地址";public static final String SHR_ORG_SERVICE = "HR系統獲取組織服務";public static final String SHR_PERSON_SERVICE = "HR系統獲取人員服務";// AD Configurationpublic static final String AD_URL = "AD域的地址";public static final String AD_ADMIN_DN = "";public static final String AD_ADMIN_PASSWORD = "管理員密碼";public static final String AD_INIT_PASSWORD = "初始密碼";public static final String AD_BASE_DN = "根OU";public static final String AD_ARCHIVED_GROUP = "封存人員組";// Status codespublic static final String STATUS_DISABLED = "1";public static final String STATUS_ENABLED = "0";public static final String PERSON_STATUS_ENABLED = "1";public static final String PERSON_STATUS_DISABLED = "0";
}
啟動類
import java.util.List;public class HrAdSynchronizer {/*定義日志對象*/private static final Logger logger = LoggerFactory.getLogger(HrAdSynchronizer.class);/*定義HR對象*/private final ShrService shrService;/*定義AD對象*/private final AdService adService;/*** 日志記錄方法*/public HrAdSynchronizer() {// 確保日志目錄存在并打印出實際路徑String logDir = SyncUtils.ensureDirectoryExists("logs");System.out.println("日志目錄: " + logDir);this.shrService = new ShrService();this.adService = new AdService();}/*** 執行方法*/public void synchronize() {try {logger.info("開始SHR到AD的同步過程");// 同步組織結構(包含變更處理)/*獲取HR中的組織信息*/List<ShrOrganization> organizations = shrService.getOrganizations();/*打印日志*/logger.info("從SHR獲取到 {} 個組織", organizations.size());/*將HR中的組織信息同步至AD*/adService.syncOrganizations(organizations);// 同步人員信息(包含變更處理)/*獲取HR中的人員信息*/List<ShrPerson> personnel = shrService.getPersonnel();/*打印日志*/logger.info("從SHR獲取到 {} 個人員", personnel.size());/*將HR中的人員信息同步至AD*/adService.syncPersonnel(personnel);/*打印日志*/logger.info("同步過程成功完成");} catch (Exception e) {logger.error("同步過程發生錯誤: {}", e.getMessage(), e);} finally {adService.close();logger.info("同步過程結束");}}/*** 啟動方法* @param args*/public static void main(String[] args) {/*打印日志,標記功能程序*/logger.info("啟動HR-AD同步程序");/*調用日志文件自動生成的方法,可注釋*/HrAdSynchronizer synchronizer = new HrAdSynchronizer();/*調用執行方法*/synchronizer.synchronize();}
}
HR組織的Bean
public class ShrOrganization {private String fnumber;private String name;private String easdeptId;private String superior;private String status;// Getters and setterspublic String getFnumber() {return fnumber;}public void setFnumber(String fnumber) {this.fnumber = fnumber;}public String getName() {return name;}public void setName(String name) {this.name = name;}public String getEasdeptId() {return easdeptId;}public void setEasdeptId(String easdeptId) {this.easdeptId = easdeptId;}public String getSuperior() {return superior;}public void setSuperior(String superior) {this.superior = superior;}public String getStatus() {return status;}public void setStatus(String status) {this.status = status;}@Overridepublic String toString() {return "ShrOrganization{" +"fnumber='" + fnumber + '\'' +", name='" + name + '\'' +", easdeptId='" + easdeptId + '\'' +", superior='" + superior + '\'' +", status='" + status + '\'' +'}';}
}
HR人員Bean
public class ShrPerson {private String empTypeName;private String mobile;private String orgNumber;private String easuserId;private String supFnumber;private String supname;private String superior;private String status;private String username;private String deptId;// Getters and setterspublic String getEmpTypeName() {return empTypeName;}public void setEmpTypeName(String empTypeName) {this.empTypeName = empTypeName;}public String getMobile() {return mobile;}public void setMobile(String mobile) {this.mobile = mobile;}public String getOrgNumber() {return orgNumber;}public void setOrgNumber(String orgNumber) {this.orgNumber = orgNumber;}public String getEasuserId() {return easuserId;}public void setEasuserId(String easuserId) {this.easuserId = easuserId;}public String getSupFnumber() {return supFnumber;}public void setSupFnumber(String supFnumber) {this.supFnumber = supFnumber;}public String getSupname() {return supname;}public void setSupname(String supname) {this.supname = supname;}public String getSuperior() {return superior;}public void setSuperior(String superior) {this.superior = superior;}public String getStatus() {return status;}public void setStatus(String status) {this.status = status;}public String getUsername() {return username;}public void setUsername(String username) {this.username = username;}public String getDeptId() {return deptId;}public void setDeptId(String deptId) {this.deptId = deptId;}@Overridepublic String toString() {return "ShrPerson{" +"empTypeName='" + empTypeName + '\'' +", mobile='" + mobile + '\'' +", orgNumber='" + orgNumber + '\'' +", easuserId='" + easuserId + '\'' +", supFnumber='" + supFnumber + '\'' +", supname='" + supname + '\'' +", superior='" + superior + '\'' +", status='" + status + '\'' +", username='" + username + '\'' +", deptId='" + deptId + '\'' +'}';}
}
獲取HR人員和組織信息的類
import com.shr.api.SHRClient;
import com.shr.api.Response;
import com.sync.config.AppConfig;
import com.sync.model.ShrOrganization;
import com.sync.model.ShrPerson;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;public class ShrService {private static final Logger logger = LoggerFactory.getLogger(ShrService.class);private final SHRClient shrClient;public ShrService() {logger.info("初始化SHR服務,連接到 {}", AppConfig.SHR_URL);this.shrClient = new SHRClient();}/*** 獲取SHR中的組織列表* @return 返回list*/public List<ShrOrganization> getOrganizations() {/*定義一個返回list對象*/List<ShrOrganization> organizations = new ArrayList<>();try {/*記錄開始調用SHR組織日志*/logger.info("調用SHR組織服務: {}", AppConfig.SHR_ORG_SERVICE);/*定義請求參數*/Map<String, Object> param = new HashMap<>();/*發起請求*/Response response = shrClient.executeService(AppConfig.SHR_URL, AppConfig.SHR_ORG_SERVICE, param);/*請求失敗處理*/if (response == null || response.getData() == null) {/*記錄失敗日志*/logger.error("從SHR獲取組織數據失敗,響應為空");/*返回失敗結果*/return organizations;}/*解析JSON數據*/JSONArray orgArray = JSON.parseArray(response.getData().toString());/*記錄json日志數量*/logger.debug("獲取到原始組織數據: {} 條記錄", orgArray.size());int enabledCount = 0;/*遍歷組織json*/for (int i = 0; i < orgArray.size(); i++) {/*獲取第i個對象*/JSONObject orgJson = orgArray.getJSONObject(i);/*獲取組織狀態*/String status = orgJson.getString("status");/*只處理啟用狀態(status=0)的組織*/if (AppConfig.STATUS_ENABLED.equals(status)) {/*定義組織對象*/ShrOrganization organization = new ShrOrganization();/*組織編碼賦值*/organization.setFnumber(orgJson.getString("fnumber"));/*組織名稱賦值*/organization.setName(orgJson.getString("name"));/*組織id賦值*/organization.setEasdeptId(orgJson.getString("easdept_id"));/*上級組織部門id賦值*/organization.setSuperior(orgJson.getString("superior"));/*組織狀態賦值*/organization.setStatus(status);/*加入list中*/organizations.add(organization);/*記錄解析日志*/logger.debug("解析啟用組織: {}", organization);/*計數器+1*/enabledCount++;} else {logger.debug("跳過禁用組織: fnumber={}, name={}",orgJson.getString("fnumber"), orgJson.getString("name"));}}/*記錄總的處理日志*/logger.info("成功解析 {} 個組織,其中啟用狀態的有 {} 個", orgArray.size(), enabledCount);} catch (Exception e) {logger.error("從SHR獲取組織信息時發生錯誤: {}", e.getMessage(), e);}return organizations;}/*** 獲取SHR中人員信息** @return 返回人員List*/public List<ShrPerson> getPersonnel() {/*定義一個List返回對象*/List<ShrPerson> personnel = new ArrayList<>();try {/*記錄開始日志*/logger.info("調用SHR人員服務: {}", AppConfig.SHR_PERSON_SERVICE);/*定義請求參數*/Map<String, Object> param = new HashMap<>();/*發起請求*/Response response = shrClient.executeService(AppConfig.SHR_URL, AppConfig.SHR_PERSON_SERVICE, param);/*請求判空*/if (response == null || response.getData() == null) {/*記錄失敗日志*/logger.error("從SHR獲取人員數據失敗,響應為空");/*返回結果*/return personnel;}/*解析JSON數據*/JSONArray personArray = JSON.parseArray(response.getData().toString());/*記錄人員數量日志*/logger.debug("獲取到原始人員數據: {} 條記錄", personArray.size());int enabledCount = 0;/*遍歷json*/for (int i = 0; i < personArray.size(); i++) {/*獲取json數據*/JSONObject personJson = personArray.getJSONObject(i);/*定義人員對象*/ShrPerson shrPerson = new ShrPerson();/*員工類型*/shrPerson.setEmpTypeName(personJson.getString("empType_name"));/*手機號*/shrPerson.setMobile(personJson.getString("mobile"));/*部門編碼*/shrPerson.setOrgNumber(personJson.getString("org_number"));/*人員ID*/shrPerson.setEasuserId(personJson.getString("easuser_id"));/*上級部門編碼*/shrPerson.setSupFnumber(personJson.getString("supFnumber"));/*上級部門名稱*/shrPerson.setSupname(personJson.getString("supname"));/*上級部門ID*/shrPerson.setSuperior(personJson.getString("superior"));/*人員狀態*/shrPerson.setStatus(personJson.getString("status"));/*人員名稱*/shrPerson.setUsername(personJson.getString("username"));/*人員所在部門ID*/shrPerson.setDeptId(personJson.getString("dept_id"));/*只添加啟用狀態的人員*/if (AppConfig.PERSON_STATUS_ENABLED.equals(shrPerson.getStatus())) {/*加入list*/personnel.add(shrPerson);/*計數器+1*/enabledCount++;/*記錄人員日志*/logger.debug("解析啟用人員: {}", shrPerson);} else {/*記錄跳過日志*/logger.debug("跳過禁用人員: easuserId={}, username={}, deptId={}",personJson.getString("easuser_id"),personJson.getString("username"),personJson.getString("dept_id"));}}/*記錄啟動狀態人數*/logger.info("成功解析 {} 個人員,其中啟用狀態的有 {} 個", personArray.size(), enabledCount);} catch (Exception e) {logger.error("從SHR獲取人員信息時發生錯誤: {}", e.getMessage(), e);}return personnel;}
}
AD中處理組織和人員的類
import com.sync.config.AppConfig;
import com.sync.model.ShrOrganization;
import com.sync.model.ShrPerson;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;import javax.naming.Context;
import javax.naming.NamingException;
import javax.naming.directory.*;
import javax.naming.ldap.Control;
import javax.naming.ldap.InitialLdapContext;
import javax.naming.ldap.LdapContext;
import javax.naming.NamingEnumeration;
import javax.naming.ldap.PagedResultsControl;
import java.io.IOException;
import java.util.*;public class AdService {/*日志對象*/private static final Logger logger = LoggerFactory.getLogger(AdService.class);/*特殊組織編碼,這些組織需要跳過處理*/private static final String SPECIAL_ORG_CODE = "999";/*記錄AD的連接*/private LdapContext ldapContext;/*緩存AD中的組織信息,用于變更檢測*/private Map<String, String> orgIdToDnMap = new HashMap<>();/*同步到AD的組織對象*/private Map<String, Attributes> orgDnToAttrsMap = new HashMap<>();/*存儲特殊組織的DN,這些組織不會被處理*/private Set<String> specialOrgDns = new HashSet<>();/*增加組織編碼到組織名稱的映射緩存*/private Map<String, String> orgNumberToNameMap = new HashMap<>();/*添加 DN 到組織名稱的映射*/private Map<String, String> dnToOuNameMap = new HashMap<>();/*構造方法*/public AdService() {initContext();// 初始化時加載現有組織結構loadExistingOrganizations();}/*AD的連接初始化*/private void initContext() {try {logger.info("初始化AD連接,URL: {}", AppConfig.AD_URL);Hashtable<String, String> env = new Hashtable<>();env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");env.put(Context.PROVIDER_URL, AppConfig.AD_URL);env.put(Context.SECURITY_AUTHENTICATION, "simple");env.put(Context.SECURITY_PRINCIPAL, AppConfig.AD_ADMIN_DN);env.put(Context.SECURITY_CREDENTIALS, AppConfig.AD_ADMIN_PASSWORD);env.put(Context.SECURITY_PROTOCOL, "ssl");ldapContext = new InitialLdapContext(env, null);logger.info("成功連接到Active Directory");} catch (NamingException e) {logger.error("連接Active Directory失敗: {}", e.getMessage(), e);}}/*** 加載AD中已存在的組織結構到緩存*/private void loadExistingOrganizations() {try {logger.info("加載AD中現有組織結構");/*定義搜索控制器*/SearchControls searchControls = new SearchControls();/*設置搜索深度*/searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);/*設置查詢內容*/String[] returnedAtts = {"distinguishedName", "ou", "adminDescription", "description"};/*設置查詢對象*/searchControls.setReturningAttributes(returnedAtts);/*設置過濾條件*/String searchFilter = "(objectClass=organizationalUnit)";/*執行查詢*/NamingEnumeration<SearchResult> results = ldapContext.search(AppConfig.AD_BASE_DN, searchFilter, searchControls);int count = 0;int specialCount = 0;int noAdminDescCount = 0;/*遍歷查詢結果*/while (results.hasMoreElements()) {/*獲取查詢結果*/SearchResult result = results.next();/*獲取dn*/String dn = result.getNameInNamespace();/*獲取其余結果*/Attributes attrs = result.getAttributes();// 保存 DN 和 OU 名稱的映射,以便后續使用if (attrs.get("ou") != null) {/*獲取ou*/String ouName = attrs.get("ou").get().toString();/*將OU放入緩存*/dnToOuNameMap.put(dn, ouName);}// 檢查是否是特殊組織boolean isSpecial = false;if (attrs.get("description") != null) {String description = attrs.get("description").get().toString();if (SPECIAL_ORG_CODE.equals(description)) {specialOrgDns.add(dn);isSpecial = true;specialCount++;logger.debug("識別到特殊組織(編碼999): {}", dn);}}if (attrs.get("ou") != null) {String ouName = attrs.get("ou").get().toString();if (AppConfig.AD_ARCHIVED_GROUP.equals(ouName)) {specialOrgDns.add(dn);isSpecial = true;specialCount++;logger.debug("識別到特殊組織(封存人員組): {}", dn);}}// 如果不是特殊組織且有adminDescription,則添加到正常組織映射if (!isSpecial && attrs.get("adminDescription") != null) {String orgId = attrs.get("adminDescription").get().toString();orgIdToDnMap.put(orgId, dn);orgDnToAttrsMap.put(dn, attrs);count++;} else if (!isSpecial) {// 記錄缺少adminDescription的組織noAdminDescCount++;orgDnToAttrsMap.put(dn, attrs);}}logger.info("已加載 {} 個組織到緩存, {} 個特殊組織被排除, {} 個組織缺少adminDescription",count, specialCount, noAdminDescCount);} catch (NamingException e) {logger.error("加載組織結構時發生錯誤: {}", e.getMessage(), e);}}/*** 同步組織到AD,處理變更情況*/public void syncOrganizations(List<ShrOrganization> organizations) {logger.info("開始同步組織到AD,共 {} 個組織", organizations.size());try {// 首先構建組織編碼到名稱的映射,用于后續定位上級組織buildOrgNumberToNameMap(organizations);// 記錄當前同步中處理過的組織ID,用于后續檢測刪除操作Set<String> processedOrgIds = new HashSet<>();/***先處理缺少adminDescription但distinguishedName匹配的組織,執行一次后,默認先不執行*/handleOrganizationsWithoutAdminDescription(organizations);// 按上級組織ID排序,確保先處理上級組織List<ShrOrganization> sortedOrgs = sortOrganizationsByHierarchy(organizations);for (ShrOrganization org : sortedOrgs) {// 跳過特殊組織編碼if (SPECIAL_ORG_CODE.equals(org.getFnumber())) {logger.info("跳過特殊組織編碼 {}: {}", org.getFnumber(), org.getName());continue;}// 跳過封存人員組if (AppConfig.AD_ARCHIVED_GROUP.equals(org.getName())) {logger.info("跳過封存人員組: {}", org.getName());continue;}String orgId = org.getEasdeptId();processedOrgIds.add(orgId);// 組織在AD中存在的DNString existingDn = orgIdToDnMap.get(orgId);//existingDn="OU=測試test,OU=集團數字化本部,OU=集團數字化部,OU=多維聯合集團股份有限公司,OU=多維聯合集團,OU=Domain Controllers,DC=duowei,DC=net,DC=cn";// 根據上級組織確定目標DNString targetDn = getTargetDnWithParent(org);//targetDn = "OU=測試test,OU=集團數字化技術部,OU=集團數字化部,OU=多維聯合集團股份有限公司,OU=多維聯合集團,OU=Domain Controllers,DC=duowei,DC=net,DC=cn";// 檢查組織狀態if (AppConfig.STATUS_DISABLED.equals(org.getStatus())) {if (existingDn != null) {logger.info("組織 {} (ID: {}) 在SHR中被禁用,標記為禁用", org.getName(), orgId);markOrganizationAsDisabled(existingDn, org);}continue;}// 處理三種情況:新建、更新屬性、重命名(移動)if (existingDn == null) {System.err.println(existingDn);// 新建組織createNewOrganization(targetDn, org);} else if (!existingDn.equals(targetDn)) {System.err.println(existingDn);// 組織名稱或層級變更,需要重命名/移動renameOrganization(existingDn, targetDn, org);} else {// 組織名稱和層級未變,但可能需要更新其他屬性updateOrganizationAttributes(existingDn, org);}}// 處理在SHR中不存在但在AD中存在的組織(刪除或禁用)handleDeletedOrganizations(processedOrgIds);logger.info("組織同步完成");} catch (Exception e) {logger.error("同步組織到AD時發生錯誤: {}", e.getMessage(), e);}}/*** 處理缺少adminDescription但distinguishedName匹配的組織*/private void handleOrganizationsWithoutAdminDescription(List<ShrOrganization> organizations) {logger.info("檢查缺少adminDescription但DN匹配的組織");int fixedCount = 0;for (ShrOrganization org : organizations) {String targetDn = getTargetDnWithParent(org);String orgId = org.getEasdeptId();// 如果organizationId不在映射中,但DN存在于AD中if (!orgIdToDnMap.containsKey(orgId) && orgDnToAttrsMap.containsKey(targetDn)) {logger.info("發現缺少adminDescription的組織,DN: {}, 組織ID: {}", targetDn, orgId);try {// 添加adminDescription屬性ModificationItem[] mods = new ModificationItem[1];mods[0] = new ModificationItem(DirContext.REPLACE_ATTRIBUTE,new BasicAttribute("adminDescription", orgId));ldapContext.modifyAttributes(targetDn, mods);// 更新緩存orgIdToDnMap.put(orgId, targetDn);Attributes attrs = orgDnToAttrsMap.get(targetDn);attrs.put("adminDescription", orgId);logger.info("成功添加adminDescription屬性到組織: {}", targetDn);fixedCount++;} catch (NamingException e) {logger.error("添加adminDescription屬性時發生錯誤: {}", e.getMessage(), e);}}}if (fixedCount > 0) {logger.info("共修復 {} 個缺少adminDescription的組織", fixedCount);}}/*** 構建組織編碼到名稱的映射*/private void buildOrgNumberToNameMap(List<ShrOrganization> organizations) {orgNumberToNameMap.clear();for (ShrOrganization org : organizations) {if (org.getFnumber() != null && org.getName() != null) {orgNumberToNameMap.put(org.getEasdeptId(), org.getName());}}logger.debug("構建了 {} 個組織編碼到名稱的映射", orgNumberToNameMap.size());}/*** 按層級關系排序組織,確保先處理上級組織*/private List<ShrOrganization> sortOrganizationsByHierarchy(List<ShrOrganization> organizations) {List<ShrOrganization> sorted = new ArrayList<>(organizations);// 首先處理沒有上級的組織,然后處理有上級的組織sorted.sort((o1, o2) -> {boolean o1HasParent = o1.getSuperior() != null && !o1.getSuperior().isEmpty();boolean o2HasParent = o2.getSuperior() != null && !o2.getSuperior().isEmpty();if (!o1HasParent && o2HasParent) return -1;if (o1HasParent && !o2HasParent) return 1;return 0;});return sorted;}/*** 根據上級組織獲取目標DN*/private String getTargetDnWithParent(ShrOrganization org) {// 額外添加查找邏輯String dn = findExistingDnByOuName(org.getName());if (dn != null) {return dn;}// 原有的邏輯作為后備if (org.getSuperior() == null || org.getSuperior().isEmpty()) {// 沒有上級組織,直接放在基礎DN下return "OU=" + org.getName() + "," + AppConfig.AD_BASE_DN;}// 查找上級組織名稱String parentNumber = org.getSuperior();String parentName = orgNumberToNameMap.get(parentNumber);if (parentName == null) {logger.warn("找不到上級組織 {},組織 {} 將直接放在基礎DN下", parentNumber, org.getName());return "OU=" + org.getName() + "," + AppConfig.AD_BASE_DN;}// 檢查上級組織是否在AD中存在String parentDN = findOrganizationDnByName(parentName);if (parentDN != null) {// 上級組織存在,將當前組織放在上級組織下return "OU=" + org.getName() + "," + parentDN;} else {logger.warn("上級組織 {} 在AD中不存在,組織 {} 將直接放在基礎DN下", parentName, org.getName());return "OU=" + org.getName() + "," + AppConfig.AD_BASE_DN;}}/*** 根據組織名稱查找DN*/private String findOrganizationDnByName(String orgName) {try {SearchControls searchControls = new SearchControls();searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);searchControls.setReturningAttributes(new String[]{"distinguishedName"});String searchFilter = "(&(objectClass=organizationalUnit)(ou=" + orgName + "))";NamingEnumeration<SearchResult> results = ldapContext.search(AppConfig.AD_BASE_DN, searchFilter, searchControls);if (results.hasMoreElements()) {SearchResult result = results.next();return result.getNameInNamespace();}} catch (NamingException e) {logger.error("查找組織 {} 時發生錯誤: {}", orgName, e.getMessage(), e);}return null;}/*** 根據OU名稱查找可能存在的DN*/private String findExistingDnByOuName(String ouName) {for (Map.Entry<String, String> entry : dnToOuNameMap.entrySet()) {if (ouName.equals(entry.getValue())) {return entry.getKey();}}return null;}/*** 創建新組織(不包含封存)*/private void createNewOrganization(String orgDn, ShrOrganization org) throws NamingException {logger.info("創建新組織: {} (ID: {})", org.getName(), org.getFnumber());Attributes attrs = new BasicAttributes();Attribute objClass = new BasicAttribute("objectClass");objClass.add("top");objClass.add("organizationalUnit");attrs.put(objClass);attrs.put("ou", org.getName());attrs.put("description", org.getFnumber());attrs.put("adminDescription", org.getEasdeptId());ldapContext.createSubcontext(orgDn, attrs);// 更新緩存orgIdToDnMap.put(org.getFnumber(), orgDn);orgDnToAttrsMap.put(orgDn, attrs);logger.info("成功創建組織: {}", org.getName());}/*** 創建封存組織* @param orgDn* @throws NamingException*/private void createFCNewOrganization(String orgDn) throws NamingException {logger.info("創建封存人員組");Attributes attrs = new BasicAttributes();Attribute objClass = new BasicAttribute("objectClass");objClass.add("top");objClass.add("organizationalUnit");attrs.put(objClass);attrs.put("ou", "封存人員組");attrs.put("description", "000");attrs.put("adminDescription", "000");ldapContext.createSubcontext(orgDn, attrs);logger.info("成功創建封存人員組");}/*** 更新組織屬性*/private void updateOrganizationAttributes(String orgDn, ShrOrganization org) throws NamingException {logger.info("更新組織屬性: {} (ID: {})", org.getName(), org.getFnumber());Attributes existingAttrs = orgDnToAttrsMap.get(orgDn);boolean hasChanges = false;List<ModificationItem> mods = new ArrayList<>();// 檢查description是否需要更新(只存放組織編碼)String currentDesc = existingAttrs.get("description") != null ?existingAttrs.get("description").get().toString() : null;String newDesc = org.getFnumber();if (currentDesc == null || !currentDesc.equals(newDesc)) {mods.add(new ModificationItem(DirContext.REPLACE_ATTRIBUTE,new BasicAttribute("description", newDesc)));hasChanges = true;}// 檢查adminDescription是否需要更新String currentAdminDesc = existingAttrs.get("adminDescription") != null ?existingAttrs.get("adminDescription").get().toString() : null;if (currentAdminDesc == null || !currentAdminDesc.equals(org.getEasdeptId())) {mods.add(new ModificationItem(DirContext.REPLACE_ATTRIBUTE,new BasicAttribute("adminDescription", org.getEasdeptId())));hasChanges = true;}if (hasChanges) {ldapContext.modifyAttributes(orgDn, mods.toArray(new ModificationItem[0]));logger.info("已更新組織 {} 的屬性", org.getName());// 更新緩存orgDnToAttrsMap.put(orgDn, ldapContext.getAttributes(orgDn));} else {logger.debug("組織 {} 的屬性無需更新", org.getName());}}/*** 重命名/移動組織*/private void renameOrganization(String oldDn, String newDn, ShrOrganization org) throws NamingException {logger.info("重命名/移動組織: 從 {} 到 {}", oldDn, newDn);// 執行重命名ldapContext.rename(oldDn, newDn);// 更新緩存orgIdToDnMap.put(org.getEasdeptId(), newDn);orgDnToAttrsMap.remove(oldDn);orgDnToAttrsMap.put(newDn, ldapContext.getAttributes(newDn));// 重命名后可能需要更新屬性updateOrganizationAttributes(newDn, org);// 更新緩存orgIdToDnMap.put(org.getEasdeptId(), newDn);orgDnToAttrsMap.remove(oldDn);orgDnToAttrsMap.put(newDn, ldapContext.getAttributes(newDn));logger.info("成功重命名/移動組織 {}", org.getName());}/*** 標記組織為禁用*/private void markOrganizationAsDisabled(String orgDn, ShrOrganization org) throws NamingException {logger.info("標記組織為禁用: {} (ID: {})", org.getName(), org.getFnumber());Attributes existingAttrs = orgDnToAttrsMap.get(orgDn);// 在description前添加"[已禁用]"標記,但保留組織編碼String currentDesc = existingAttrs.get("description") != null ?existingAttrs.get("description").get().toString() : org.getFnumber();if (!currentDesc.startsWith("[已禁用]")) {ModificationItem[] mods = new ModificationItem[1];mods[0] = new ModificationItem(DirContext.REPLACE_ATTRIBUTE,new BasicAttribute("description", "[已禁用] " + org.getFnumber()));ldapContext.modifyAttributes(orgDn, mods);// 更新緩存orgDnToAttrsMap.put(orgDn, ldapContext.getAttributes(orgDn));}logger.info("組織 {} 已標記為禁用", org.getName());}/*** 處理已刪除的組織*/private void handleDeletedOrganizations(Set<String> processedOrgIds) throws NamingException {logger.info("處理在SHR中不存在的組織");for (String orgId : orgIdToDnMap.keySet()) {if (!processedOrgIds.contains(orgId)) {String orgDn = orgIdToDnMap.get(orgId);// 跳過特殊組織if (specialOrgDns.contains(orgDn)) {logger.info("跳過特殊組織的刪除處理: {}", orgDn);continue;}// 獲取現有屬性Attributes attrs = orgDnToAttrsMap.get(orgDn);String description = attrs.get("description") != null ?attrs.get("description").get().toString() : "";// 如果是特殊編碼,跳過if (SPECIAL_ORG_CODE.equals(description)) {logger.info("跳過特殊編碼組織的刪除處理: {}", orgDn);continue;}// 如果描述中沒有已刪除標記,添加標記if (!description.startsWith("[已刪除]")) {ModificationItem[] mods = new ModificationItem[1];mods[0] = new ModificationItem(DirContext.REPLACE_ATTRIBUTE,new BasicAttribute("description", "[已刪除] " + description));ldapContext.modifyAttributes(orgDn, mods);logger.info("標記組織為已刪除: {}", orgDn);// 更新緩存orgDnToAttrsMap.put(orgDn, ldapContext.getAttributes(orgDn));}}}}/*** 同步人員到AD,處理各種變更情況*/public void syncPersonnel(List<ShrPerson> personnel) {logger.info("開始同步人員到AD,共 {} 個人員", personnel.size());try {// 確保封存組存在String archiveGroupDN = "OU=" + AppConfig.AD_ARCHIVED_GROUP + "," + AppConfig.AD_BASE_DN;if (!checkIfEntryExists(archiveGroupDN)) {logger.info("封存人員組不存在,開始創建");createFCNewOrganization(archiveGroupDN);}// 加載AD中現有用戶Map<String, UserAdInfo> existingUsers = loadExistingUsers();logger.info("已加載 {} 個AD用戶到緩存", existingUsers.size());// 記錄處理過的用戶ID,用于后續檢測刪除操作Set<String> processedUserIds = new HashSet<>();int createdCount = 0;int movedCount = 0;int updatedCount = 0;int disabledCount = 0;int skippedCount = 0;// 同步用戶for (ShrPerson person : personnel) {try {///*測試*///if(!person.getUsername().equals("宋汝東")){// continue;//}// 1. 基本檢查if (person.getEasuserId() == null || person.getEasuserId().isEmpty()) {logger.warn("跳過無ID的用戶: {}", person);skippedCount++;continue;}if (person.getUsername() == null || person.getUsername().isEmpty()) {logger.warn("跳過無用戶名的用戶: {}", person.getEasuserId());skippedCount++;continue;}// 2. 檢查員工類型if (!isValidEmployeeType(person.getEmpTypeName())) {logger.debug("跳過非目標類型員工: {} (類型: {})",person.getUsername(), person.getEmpTypeName());skippedCount++;continue;}String userId = person.getEasuserId();processedUserIds.add(userId);// 3. 員工在AD中的信息UserAdInfo userInfo = existingUsers.get(userId);boolean exists = (userInfo != null);// 4. 處理禁用用戶if (AppConfig.PERSON_STATUS_DISABLED.equals(person.getStatus())) {if (exists) {logger.info("用戶 {} 在SHR中被禁用,移至封存組并禁用", person.getUsername());disableAndArchiveUser(userInfo.getDn(), archiveGroupDN, person);disabledCount++;}continue;}// 5. 確定用戶所屬組織DNString orgDN = findOrgDnByDeptId(person.getDeptId());if (orgDN == null) {logger.warn("找不到用戶 {} 所屬組織(deptId={}), 將使用默認組織",person.getUsername(), person.getDeptId());orgDN = AppConfig.AD_BASE_DN;}// 6. 生成目標DN - 使用用戶名而不是IDString targetUserDN = "CN=" + person.getUsername() + "," + orgDN;//if(person.getUsername().equals("田振強")){// System.out.println(111111);//}// 7. 處理不同情況if (!exists) {//System.err.println(person.getUsername());// 用戶不存在 - 新建用戶createNewUser(targetUserDN, person);createdCount++;} else if (!userInfo.getDn().equals(targetUserDN)) {//System.err.println(person.getUsername());// 用戶存在但DN不同 - 移動用戶moveUser(userInfo.getDn(), targetUserDN, person);movedCount++;} else {//System.err.println(person.getUsername());// 用戶存在且DN一致 - 更新屬性updateUserAttributes(userInfo.getDn(), person);updatedCount++;}} catch (Exception e) {logger.error("處理用戶 {} 時發生錯誤: {}", person.getUsername(), e.getMessage(), e);}}// 8. 處理已刪除的用戶int deletedCount = handleDeletedUsers(existingUsers, processedUserIds, archiveGroupDN);logger.info("人員同步完成 - 新建: {}, 移動: {}, 更新: {}, 禁用: {}, 刪除: {}, 跳過: {}",createdCount, movedCount, updatedCount, disabledCount, deletedCount, skippedCount);//logger.info("人員同步完成 - 新建: {}, 移動: {}, 更新: {}, 禁用: {}, 刪除: {}, 跳過: {}",// createdCount, movedCount, updatedCount, disabledCount, skippedCount);} catch (NamingException e) {logger.error("同步人員到AD時發生錯誤: {}", e.getMessage(), e);} catch (IOException e) {throw new RuntimeException(e);}}/*** 判斷是否為有效的員工類型*/private boolean isValidEmployeeType(String empTypeName) {if (empTypeName == null) return false;// 只處理正式員工、試用員工、實習的人員return empTypeName.contains("正式") ||empTypeName.contains("試用") ||empTypeName.contains("實習");}/*** 加載AD中現有用戶到緩存*/private Map<String, UserAdInfo> loadExistingUsers() throws NamingException, IOException {Map<String, UserAdInfo> userMap = new HashMap<>();SearchControls searchControls = new SearchControls();searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);String[] returnedAtts = {"distinguishedName", "info", "userAccountControl", "cn"};searchControls.setReturningAttributes(returnedAtts);String searchFilter = "(&(objectClass=user))";ldapContext.setRequestControls(new Control[]{new PagedResultsControl(10000, Control.NONCRITICAL)});NamingEnumeration<SearchResult> results = ldapContext.search(AppConfig.AD_BASE_DN, searchFilter, searchControls);while (results.hasMoreElements()) {SearchResult result = results.next();String dn = result.getNameInNamespace();Attributes attrs = result.getAttributes();// 使用info屬性(對應easuserId)作為用戶IDif (attrs.get("info") != null) {String userId = attrs.get("info").get().toString();// 獲取用戶賬戶控制屬性,判斷是否禁用boolean disabled = false;if (attrs.get("userAccountControl") != null) {int uac = Integer.parseInt(attrs.get("userAccountControl").get().toString());disabled = (uac & 2) != 0; // 賬戶禁用標志是第2位}// 獲取用戶顯示名稱String displayName = "";if (attrs.get("cn") != null) {displayName = attrs.get("cn").get().toString();}userMap.put(userId, new UserAdInfo(userId, dn, disabled, displayName));}}//System.err.println(personCount);return userMap;}/*** 創建新用戶*/private void createNewUser(String userDN, ShrPerson person) throws NamingException {logger.info("創建新用戶: {} (ID: {})", person.getUsername(), person.getEasuserId());Attributes attrs = new BasicAttributes();Attribute objClass = new BasicAttribute("objectClass");objClass.add("top");objClass.add("person");objClass.add("organizationalPerson");objClass.add("user");attrs.put(objClass);// CN已經包含在DN中,使用用戶名attrs.put("cn", person.getUsername());// 使用手機號作為登錄名if (person.getMobile() != null && !person.getMobile().isEmpty()) {attrs.put("sAMAccountName", person.getMobile());attrs.put("userPrincipalName", person.getMobile() + "@duowei.net.cn");} else {// 如果沒有手機號,回退到使用用戶IDlogger.warn("用戶 {} 沒有手機號,將使用ID作為登錄名", person.getUsername());attrs.put("sAMAccountName", person.getEasuserId());attrs.put("userPrincipalName", person.getEasuserId() + "@duowei.net.cn");}// 設置顯示名稱attrs.put("displayName", person.getUsername());// 將easuserId存入info屬性attrs.put("info", person.getEasuserId());// 設置姓和名// 假設中文名格式為"姓+名",取第一個字為姓,其余為名if (person.getUsername() != null && !person.getUsername().isEmpty()) {String fullName = person.getUsername();if (fullName.length() > 1) {// 取第一個字為姓String lastName = fullName.substring(0, 1);// 取剩余部分為名String firstName = fullName.substring(1);attrs.put("sn", lastName);attrs.put("givenName", firstName);} else {// 如果只有一個字,則全部作為姓attrs.put("sn", fullName);}}// 其他屬性if (person.getMobile() != null) {attrs.put("mobile", person.getMobile());}if (person.getDeptId() != null) {attrs.put("department", person.getDeptId());}// 設置密碼byte[] unicodePwd = generatePassword(AppConfig.AD_INIT_PASSWORD);attrs.put(new BasicAttribute("unicodePwd", unicodePwd));// 用戶控制標志: 正常賬戶 + 密碼不過期int userAccountControl = 512 | 65536;attrs.put(new BasicAttribute("userAccountControl", String.valueOf(userAccountControl)));// 要求下次登錄更改密碼attrs.put(new BasicAttribute("pwdLastSet", "0"));// 創建用戶ldapContext.createSubcontext(userDN, attrs);}/*** 移動用戶到新位置*/private void moveUser(String currentDN, String targetDN, ShrPerson person) throws NamingException {logger.info("移動用戶: {} 從 {} 到 {}", person.getUsername(), currentDN, targetDN);try {// 執行重命名操作移動用戶ldapContext.rename(currentDN, targetDN);// 移動后更新屬性updateUserAttributes(targetDN, person);} catch (NamingException e) {logger.error("移動用戶 {} 時發生錯誤: {}", person.getUsername(), e.getMessage());throw e;}}/*** 更新用戶屬性*/private void updateUserAttributes(String userDN, ShrPerson person) throws NamingException {logger.debug("更新用戶屬性: {}", person.getUsername());List<ModificationItem> mods = new ArrayList<>();// 更新手機號if (person.getMobile() != null) {mods.add(new ModificationItem(DirContext.REPLACE_ATTRIBUTE,new BasicAttribute("mobile", person.getMobile())));}// 更新部門IDif (person.getDeptId() != null) {mods.add(new ModificationItem(DirContext.REPLACE_ATTRIBUTE,new BasicAttribute("department", person.getDeptId())));}/*更新登錄名為手機號*/if(person.getMobile() != null){mods.add(new ModificationItem(DirContext.REPLACE_ATTRIBUTE,new BasicAttribute("sAMAccountName", person.getMobile())));}// 更新info屬性(easuserId)mods.add(new ModificationItem(DirContext.REPLACE_ATTRIBUTE,new BasicAttribute("info", person.getEasuserId())));// 確保賬戶處于啟用狀態mods.add(new ModificationItem(DirContext.REPLACE_ATTRIBUTE,new BasicAttribute("userAccountControl", "512")));// 應用修改if (!mods.isEmpty()) {ModificationItem[] modsArray = mods.toArray(new ModificationItem[0]);ldapContext.modifyAttributes(userDN, modsArray);}}/*** 禁用用戶并移動到歸檔組*/private void disableAndArchiveUser(String userDN, String archiveGroupDN, ShrPerson person) throws NamingException {logger.info("禁用并歸檔用戶: {}", person.getUsername());try {// 首先禁用用戶ModificationItem[] disableMods = new ModificationItem[1];disableMods[0] = new ModificationItem(DirContext.REPLACE_ATTRIBUTE,new BasicAttribute("userAccountControl", "514")); // 514 = 禁用賬戶ldapContext.modifyAttributes(userDN, disableMods);// 然后移動到歸檔組String userName = person.getUsername();String newDN = "CN=" + userName + "," + archiveGroupDN;ldapContext.rename(userDN, newDN);} catch (NamingException e) {logger.error("禁用并歸檔用戶 {} 時發生錯誤: {}", person.getUsername(), e.getMessage());throw e;}}/*** 處理已刪除的用戶*/private int handleDeletedUsers(Map<String, UserAdInfo> existingUsers, Set<String> processedUserIds,String archiveGroupDN) throws NamingException {logger.info("處理已刪除用戶");int count = 0;for (UserAdInfo userInfo : existingUsers.values()) {String userId = userInfo.getUserId();// 如果用戶未在當前處理列表中,且不在歸檔組,則歸檔if (!processedUserIds.contains(userId) && !isInArchiveGroup(userInfo.getDn(), archiveGroupDN)) {logger.info("用戶ID {} 在SHR中不存在,移至封存組并禁用", userId);try {disableUser(userInfo.getDn());moveUserToArchiveGroup(userInfo.getDn(), archiveGroupDN);count++;} catch (NamingException e) {logger.error("處理已刪除用戶 {} 時發生錯誤: {}", userId, e.getMessage());}}}return count;}/*** 檢查用戶是否已在歸檔組中*/private boolean isInArchiveGroup(String userDN, String archiveGroupDN) {return userDN.endsWith(archiveGroupDN);}/*** 用戶AD信息類*/private static class UserAdInfo {private final String userId;private final String dn;private final boolean disabled;private final String displayName;public UserAdInfo(String userId, String dn, boolean disabled, String displayName) {this.userId = userId;this.dn = dn;this.disabled = disabled;this.displayName = displayName;}public String getUserId() {return userId;}public String getDn() {return dn;}public boolean isDisabled() {return disabled;}public String getDisplayName() {return displayName;}}public void close() {try {if (ldapContext != null) {ldapContext.close();logger.info("關閉LDAP連接");}} catch (NamingException e) {logger.error("關閉LDAP連接時發生錯誤: {}", e.getMessage(), e);}}/*** 根據部門ID查找組織DN*/private String findOrgDnByDeptId(String deptId) {if (deptId == null || deptId.isEmpty()) {return null;}return orgIdToDnMap.get(deptId);}/*** 從組織 DN 中提取組織名稱*/private String getOrgNameFromDN(String dn) {if (dn == null || dn.isEmpty()) {return "未知組織";}try {// DN 格式通常是 "OU=組織名稱,其他部分"// 提取第一個 OU= 后面的內容,直到下一個逗號if (dn.contains("OU=")) {int start = dn.indexOf("OU=") + 3; // OU= 后面的位置int end = dn.indexOf(",", start);if (end > start) {return dn.substring(start, end);} else {return dn.substring(start);}}// 如果沒有找到 OU=,嘗試從 dnToOuNameMap 獲取if (dnToOuNameMap.containsKey(dn)) {return dnToOuNameMap.get(dn);}} catch (Exception e) {logger.warn("無法從DN提取組織名稱: {}", dn);}return "未知組織";}/*** 檢查指定DN的條目是否存在*/private boolean checkIfEntryExists(String dn) {try {ldapContext.lookup(dn);return true;} catch (NamingException e) {return false;}}/*** 生成AD密碼* AD密碼需要以特定格式提供,使用Unicode編碼*/private byte[] generatePassword(String password) {// 將密碼轉換為AD要求的Unicode字節格式String quotedPassword = "\"" + password + "\"";char[] unicodePwd = quotedPassword.toCharArray();byte[] pwdBytes = new byte[unicodePwd.length * 2];// 轉換為Unicode格式for (int i = 0; i < unicodePwd.length; i++) {pwdBytes[i * 2] = (byte) (unicodePwd[i] & 0xff);pwdBytes[i * 2 + 1] = (byte) (unicodePwd[i] >> 8);}return pwdBytes;}/*** 禁用用戶賬戶*/private void disableUser(String userDN) throws NamingException {logger.info("禁用用戶: {}", userDN);// 用戶賬戶控制: 禁用賬戶 (514)ModificationItem[] mods = new ModificationItem[1];mods[0] = new ModificationItem(DirContext.REPLACE_ATTRIBUTE,new BasicAttribute("userAccountControl", "514"));ldapContext.modifyAttributes(userDN, mods);}/*** 將用戶移動到封存組*/private void moveUserToArchiveGroup(String userDN, String archiveGroupDN) throws NamingException {logger.info("移動用戶到封存組: {} -> {}", userDN, archiveGroupDN);// 獲取用戶DN中的CN部分String cn = "";if (userDN.startsWith("CN=")) {int endIndex = userDN.indexOf(',');if (endIndex > 0) {cn = userDN.substring(0, endIndex);} else {cn = userDN;}} else {// 如果不是以CN=開頭,使用整個DNcn = "CN=" + getDnFirstComponent(userDN);}String newDN = cn + "," + archiveGroupDN;// 執行移動操作ldapContext.rename(userDN, newDN);}/*** 從DN中提取第一個組件*/private String getDnFirstComponent(String dn) {if (dn == null || dn.isEmpty()) {return "";}// DN格式可能是 "CN=名稱,OU=組織,..."if (dn.contains("=")) {int startIndex = dn.indexOf('=') + 1;int endIndex = dn.indexOf(',', startIndex);if (endIndex > startIndex) {return dn.substring(startIndex, endIndex);} else {return dn.substring(startIndex);}}return dn;}
}
日志配置
<configuration><property name="LOG_PATH" value="D:/ADsync/logs" /><property name="FILE_NAME" value="AdSync" /><appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"><rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy"><!-- 日志文件命名規則 --><fileNamePattern>D:/ADsync/logs/AdSync.%d{yyyy-MM-dd}.%i.log</fileNamePattern><!-- 單個日志文件最大大小 --><maxFileSize>10MB</maxFileSize><!-- 保留最近 30 天的日志 --><maxHistory>30</maxHistory><!-- 總日志文件大小限制 --><totalSizeCap>1GB</totalSizeCap></rollingPolicy><encoder><pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern></encoder></appender><appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"><encoder><pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern></encoder></appender><root level="INFO"><appender-ref ref="FILE" /><appender-ref ref="CONSOLE" /></root>
</configuration>
POM.xml文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com</groupId><artifactId>hr-ad-synchronizer</artifactId><version>1.0</version><packaging>jar</packaging><properties><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><maven.compiler.source>1.8</maven.compiler.source><maven.compiler.target>1.8</maven.compiler.target></properties><dependencies><!-- HTTP客戶端 --><dependency><groupId>org.apache.httpcomponents</groupId><artifactId>httpclient</artifactId><version>4.5.13</version></dependency><!-- JSON處理 --><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>2.0.51</version></dependency><!-- 日志框架 --><dependency><groupId>org.slf4j</groupId><artifactId>slf4j-api</artifactId><version>1.7.36</version></dependency><dependency><groupId>ch.qos.logback</groupId><artifactId>logback-classic</artifactId><version>1.2.11</version><scope>runtime</scope></dependency><!-- Apache Axis相關 --><dependency><groupId>org.apache.axis</groupId><artifactId>axis</artifactId><version>1.4</version></dependency><dependency><groupId>commons-discovery</groupId><artifactId>commons-discovery</artifactId><version>0.5</version></dependency><dependency><groupId>commons-logging</groupId><artifactId>commons-logging</artifactId><version>1.1.1</version></dependency><dependency><groupId>wsdl4j</groupId><artifactId>wsdl4j</artifactId><version>1.6.2</version></dependency><!-- SHR API依賴 --><dependency><groupId>com.shr</groupId><artifactId>api</artifactId><version>1.0</version></dependency></dependencies><build><finalName>hr-ad-sync</finalName><plugins><!-- 編譯插件 --><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.8.1</version><configuration><source>1.8</source><target>1.8</target><encoding>UTF-8</encoding></configuration></plugin><!-- 使用 assembly 插件,它更簡單且可靠 --><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-assembly-plugin</artifactId><version>3.3.0</version><configuration><descriptorRefs><descriptorRef>jar-with-dependencies</descriptorRef></descriptorRefs><archive><manifest><mainClass>com.sync.HrAdSynchronizer</mainClass></manifest></archive></configuration><executions><execution><id>make-assembly</id><phase>package</phase><goals><goal>single</goal></goals></execution></executions></plugin></plugins></build>
</project>
生成EXE文件
1、通過MAVEN打jar包
2、下載launch4j
3、通過launch4j生成exe文件:https://blog.csdn.net/qq_41804823/article/details/145967426
服務器定時任務
1、打開服務器管理
2、點擊右上角“工具”,打開任務計劃程序
3、新增任務計劃程序庫
異常問題注意事項
1、測試時,增加基礎OU限制
2、出現權限異常問題,先檢查賦值是否正確