先讲下背景吧。最近工作重心在医院,做医院系统。因为我们公司是做平台的,其中有一个系统是单点登录,需要同步医院里其它系统的所有帐号。如果单点登录系统帐号变动,会把相应的帐号信息推送到其它系统。比如:添加或者改变某一个帐号的信息,那就需要把当前帐号信息推送到相应的系统;又比如:角色变动了,就把所有与此角色关系的帐号信息及权限信息同步到相应的系统。
单点登录系统功能早就完成并测试完毕,如果在原来的功能上添加,又需要重新测试一次,既费时费力,又不符合软件设计原则中的:开闭原则。
为了实现这个功能,我想到了2种方式:
-
监听 监听 ServletRequestListener 中的 requestInitialized 方法,在配制文件里面配制需要监听的地址,一但请求进来,符合要求就会进入处理。
-
AOP 切面 实现一个切面功能,并定义一个注解,在每个变动的接口方法上加上定义的切面注解即可。
两种方式都各有优缺点,先来说它们各自优点
监听优点:
-
基于配制模式,把需要监听的地址都是放在配制文件里,方便修改
-
功能实现简单
-
功能扩展很方便
监听缺点:
-
是监听的请求,每个请求进来后,都会在此做过滤,其实监听的只是极少数地址,这样做感觉没必要
-
如果对用户信息操作失败,也会产生用户数据。实际场景时,操作失败没有用户信息数据推送
-
如果是处理比较耗时的同步方法,会对接口的QPS有很大的影响
AOP 切面优点:
-
粒度更细,精确到了具体的某个方法
-
对单个用户信息的变动能精准识别
-
如果接口响应失败,可不用产生数据,也可产生数据(需要程序员在处理过程中选择)
AOP 切面缺点:
-
难度相对监听增大了
-
需要在具体的方法上增加注解
以上2种方式都实现了,我都把详细的代码贴出来,大家可根据自己项目需求选择。
监听实现:
先说一下监听的原理:一个请求过来后,根据请求的地址做一个拦截,如果是在拦截队列中,实现 ServletRequestListener 接口的 requestInitialized 方法, 或者 requestDestroyed 请求结束后的方法
//@WebListener
@Slf4j
public class MyServletRequestListener implements ServletRequestListener {
@SneakyThrows
@Override
public void requestInitialized(ServletRequestEvent requestEvent) {
if (!(requestEvent.getServletRequest() instanceof HttpServletRequest)) {
throw new IllegalArgumentException(
"Request is not an HttpServletRequest: " + requestEvent.getServletRequest());
}
HttpServletRequest request = (HttpServletRequest) requestEvent.getServletRequest();
String url = request.getRequestURI();
if(StringUtils.hasText(url) && platformProperties.getListenUrl() != null
&& platformProperties.getListenUrl().size() > 0 && platformProperties.getListenUrl().contains(url)){
queryUserAble.setClientId(platformProperties.getSendUserListToApp());
queryUserAble.setRequest(request);
gftechAutoConfiguration.executorService().execute(queryUserAble);
sendMsgToCenterServer.sendMsg();
}
}
@Override
public void requestDestroyed(ServletRequestEvent requestEvent) {
}
}
注意2点:
-
使用此方法需要添加 @WebListener 和 @ServletComponentScan 注解
-
在这里不太耗时,否则对接口响应有影响。建议用线程池处理任务
AOP 切面实现
首先:定义一个注解
package com.gftech.ms.annotation;
public @interface MyAspect {
}
再次:实现切面功能
package com.gftech.ms.configuration;
@Aspect
@Component
@Slf4j
public class SendUserAspect{
@AfterReturning(value = "@annotation(com.gftech.ms.annotation.MyAspect)", returning="result")
public void computerSendUser(JoinPoint joinPoint, Object result){
if(result instanceof Response<?>){
Response<?> response = (Response<?>) result;
if("SUCCESS".equals(response.getMessage())){
HttpServletRequest request = ((ServletRequestAttributes)Objects.requireNonNull(RequestContextHolder.currentRequestAttributes())).getRequest();
// 这里写你 业务代码
}
}
}
}
最后:在需要的方法上添加 @MyAspect 注解即可
我在我的项目中使用 UserRunAble 来代表任务,以下是定义:
package com.gftech.ms.runable;
@Slf4j
@Data
@Component
public class UserRunAble implements Runnable {
private JoinPoint joinPoint;
private String functionName;
private HttpServletRequest request;
private String workStation;
@Resource
private ChangeStateImpl changeUserStateImpl;
@Resource
private AddRoleImpl addRoleImpl;
@Resource
private EditImpl editImpl;
@SneakyThrows
@Override
public void run() {
functionName = joinPoint.getSignature().getName();
if(StringUtils.isBlank(functionName)){ return; }
Object[] objects = joinPoint.getArgs();
if(objects.length <= 0){ return; }
switch (functionName){
case "changeState" :
changeUserStateImpl.setWorkStation(workStation);
UserRunAbleInterface userRunAbleInterface = UserRunAbleInterface.userRunAbleInterfaceMap.get("changeStateImpl");
userRunAbleInterface.saveData(objects[0]);
break;
case "insert" :
case "edit" :
editImpl.setWorkStation(workStation);
UserRunAbleInterface edit = UserRunAbleInterface.userRunAbleInterfaceMap.get("editImpl");
edit.saveData(objects[0]);
break;
case "editRole":
case "addRole":
addRoleImpl.setHttpServletRequest(request);
addRoleImpl.setWorkStation(workStation);
UserRunAbleInterface addRole = UserRunAbleInterface.userRunAbleInterfaceMap.get("addRoleImpl");
addRole.saveData(objects[0]);
break;
default:
throw new ApiException("切面处理用户信息:未知的方法");
}
}
}
定义的接口:
package com.gftech.ms.runable;
import com.gftech.ms.runable.vo.MhSendUserToApp;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
public interface UserRunAbleInterface {
HashMap<String, UserRunAbleInterface> userRunAbleInterfaceMap = new HashMap<>();
List<MhSendUserToApp> getMhSendUserToApp(Object object) throws IOException;
void saveData(Object object) throws IOException;
}
定论的模板类:
package com.gftech.ms.runable;
import com.gftech.ms.configuration.PlatformProperties;
@Slf4j
public abstract class UserRunAblePlate implements UserRunAbleInterface {
private List<String> appIdList;
private MhSendUserToAppMapper mhSendUserToAppMapper;
private RoleManageServiceImpl roleManageService;
private SendMsgToCenterServer sendMsgToCenterServer;
public UserRunAblePlate(MhSendUserToAppMapper mhSendUserToAppMapper, RoleManageServiceImpl roleManageService, PlatformProperties platformProperties, SendMsgToCenterServer sendMsgToCenterServer){
this.mhSendUserToAppMapper = mhSendUserToAppMapper;
this.roleManageService = roleManageService;
this.sendMsgToCenterServer = sendMsgToCenterServer;
if(StringUtils.isNotBlank(platformProperties.getSendUserListToApp())){
this.appIdList = new ArrayList<>(Arrays.asList(platformProperties.getSendUserListToApp().split(",")));
}
}
@Override
public List<MhSendUserToApp> getMhSendUserToApp(Object object) throws IOException {
return SubGetMhSendUserToApp(object);
}
@Override
public void saveData(Object object) throws IOException {
saveToTable(object);
}
public abstract List<MhSendUserToApp> SubGetMhSendUserToApp(Object object) throws IOException;
protected HashMap<String, NodeDataExt> getExistHashMap(List<NodeDataExt> nodeDataExtList){
HashMap<String, NodeDataExt> extHashMap = new HashMap<>();
for (NodeDataExt item : nodeDataExtList){
}
return extHashMap;
}
protected List<NodeDataExt> getNodeDataExt(List<String> roleIds){
if(roleIds.isEmpty()) return null;
return roleManageService.getAppTreeByRoleIds( roleIds);
}
private void saveToTable(Object object) throws IOException {
}
}
AddRoleImpl 的实现:
package com.gftech.ms.runable.impl;
import java.util.stream.Collectors;
@Service("addRoleImpl")
public class AddRoleImpl extends UserRunAblePlate {
private HttpServletRequest request;
private List<String> appIdList;
private String workStation;
@Autowired
public AddRoleImpl(MhSendUserToAppMapper mhSendUserToAppMapper, RoleManageServiceImpl roleManageService, PlatformProperties platformProperties, SendMsgToCenterServer sendMsgToCenterServer) {
super(mhSendUserToAppMapper, roleManageService, platformProperties, sendMsgToCenterServer);
if(org.apache.commons.lang3.StringUtils.isNotBlank(platformProperties.getSendUserListToApp())){
appIdList = new ArrayList<>(Arrays.asList(platformProperties.getSendUserListToApp().split(",")));
}
UserRunAbleInterface.userRunAbleInterfaceMap.put("addRoleImpl", this);
}
public void setHttpServletRequest(HttpServletRequest request){
this.request = request;
}
public void setWorkStation(String workStation){
this.workStation = workStation;
}
@Override
public List<MhSendUserToApp> SubGetMhSendUserToApp(Object object) throws IOException {
if(object == null){
return new ArrayList<>();
}
return listUser;
}
}
ChangeStateImpl 的实现:
package com.gftech.ms.runable.impl;
@Slf4j
@Service("changeStateImpl")
public class ChangeStateImpl extends UserRunAblePlate {
private List<String> appIdList;
private String workStation;
@Autowired
public ChangeStateImpl(MhSendUserToAppMapper mhSendUserToAppMapper, RoleManageServiceImpl roleManageService, PlatformProperties platformProperties, SendMsgToCenterServer sendMsgToCenterServer) {
super(mhSendUserToAppMapper, roleManageService, platformProperties, sendMsgToCenterServer);
if(StringUtils.isNotBlank(platformProperties.getSendUserListToApp())){
appIdList = new ArrayList<>(Arrays.asList(platformProperties.getSendUserListToApp().split(",")));
}
UserRunAbleInterface.userRunAbleInterfaceMap.put("changeStateImpl", this);
}
public void setWorkStation(String workStation){
this.workStation = workStation;
}
@Override
public List<MhSendUserToApp> SubGetMhSendUserToApp(Object object) throws IOException {
if(object == null){
return new ArrayList<>();
}
return listUser;
}
}
EditImpl 的实现:
package com.gftech.ms.runable.impl;
@Service("editImpl")
public class EditImpl extends UserRunAblePlate {
private String workStation;
private List<String> appIdList;
@Autowired
public EditImpl(MhSendUserToAppMapper mhSendUserToAppMapper, RoleManageServiceImpl roleManageService, PlatformProperties platformProperties, SendMsgToCenterServer sendMsgToCenterServer) {
super(mhSendUserToAppMapper, roleManageService, platformProperties, sendMsgToCenterServer);
if(StringUtils.isNotBlank(platformProperties.getSendUserListToApp())){
appIdList = new ArrayList<>(Arrays.asList(platformProperties.getSendUserListToApp().split(",")));
}
UserRunAbleInterface.userRunAbleInterfaceMap.put("editImpl", this);
}
public void setWorkStation(String workStation){
this.workStation = workStation;
}
@Override
public List<MhSendUserToApp> SubGetMhSendUserToApp(Object object) throws IOException {
if(object == null){
return new ArrayList<>();
}
return listUser;
}
}
好了,根据切面思想就完成了。
程序的执行顺序:
SendUserAspect -> UserRunAble
AddRoleImpl,ChangeStateImpl,EditImpl 这3个都是UserRunAblePlate 类的实现类,都注入到了IOC容器,并且在各自的构造方法中,把自己添加到了 UserRunAbleInterface 接口中的 HashMap 中
HashMap<String, UserRunAbleInterface> userRunAbleInterfaceMap = new HashMap<>();
在 UserRunAblePlate 类中,实现了接口 UserRunAbleInterface 中定义的2个方法,并且定义了一个抽象方法由子类实现:
public abstract List<MhSendUserToApp> SubGetMhSendUserToApp(Object object) throws IOException;
SubGetMhSendUserToApp 这个方法其实是最核心的方法,在我的程序里,都是它在处理帐号信息。
这里再补充一个面试经常被问的点。AOP是JDK动态代理实现的,是在Bean生命周期后置增加环节实现的,也是在Bean初始化之后。