在项目迭代开发中经常会遇到对已有功能的改造需求,尽管我们可能已经预留了扩展点,并且尝试通过接口或扩展类完成此类任务。可是,仍然有很多难以预料的场景无法通过上述方式解决。修改原有代码当然能够做到,但是这会增加许多附加成本,回归测试带来大量工作和一些潜在的未知风险。特别是一些极其重要的公共模块,可谓牵一发而动全身,稍有不慎都将引发重大的故障。这里分享一下自己在项目开发中的一点实践,一种基于AOP的插件化(扩展)方案。
假设一个场景:
现有一个可获取人员信息的服务:UserService。
public class UserService { public User getUserById(String id) { // ... } }
该服务作为一个基础服务一直运行良好,但是客户出于对数据安全的考虑需要对人员信息中某些进行属性进行脱敏。该需求总体来说不是很复杂,解决方案也不止一个,那么来看看基于AOP的实现方式是怎么样的。
插件点注解(定义一次,多处可用):
@Documented @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Component public @interface PluginPoint { Class<?> service(); String method(); PluginType type(); enum PluginType { BEFORE, AFTER, OVERRIDE; } }
插件服务接口(后期所有的插件都必须实现这个接口):
public interface PluginService { Object process(Object[] args, Object result) throws Exception; }
处理插件业务的切面类:
@Aspect @Component public class PluginServiceAspect { private static Logger logger = LoggerFactory.getLogger(PluginServiceAspect.class); private static ConcurrentHashMap<String, PluginService> pluginConfigMap = new ConcurrentHashMap<String, PluginService>(); private static volatile Boolean initStatus = false; @Around("execution(* com.lichmama.demo..service.*Service.*(..))") public Object around(ProceedingJoinPoint point) throws Throwable { PluginService pluginService = getPluginService(point); if (pluginService == null) { return point.proceed(); } PluginPoint pluginPoint = pluginService.getClass().getAnnotation(PluginPoint.class); Object result = null; // before if (pluginPoint.type() == PluginType.BEFORE) { pluginService.process(point.getArgs(), null); } // override if (pluginPoint.type() == PluginType.OVERRIDE) { result = pluginService.process(point.getArgs(), null); } // after if (pluginPoint.type() == PluginType.AFTER) { result = pluginService.process(point.getArgs(), point.proceed()); } return result; } private PluginService getPluginService(ProceedingJoinPoint point) { // initialize pluginConfigMap first initPluginConfigMap(); MethodSignature signature = (MethodSignature) point.getSignature(); Method method = signature.getMethod(); String className = method.getDeclaringClass().getName(); String methodName = method.getName(); String serviceMethod = className + "#" + methodName; return pluginConfigMap.get(serviceMethod); } private void loadPluginService() { Map<String, Object> plugins = SpringContextHolder.getBeansWithAnnotation(PluginPoint.class); if (plugins != null && plugins.size() > 0) { Iterator<Map.Entry<String, Object>> iter = plugins.entrySet().iterator(); while (iter.hasNext()) { Map.Entry<String, Object> entry = iter.next(); PluginService pluginService = (PluginService) entry.getValue(); PluginPoint pluginPoint = pluginService.getClass().getAnnotation(PluginPoint.class); Class<?> service = pluginPoint.service(); String method = pluginPoint.method(); String serviceMethod = service.getName() + "#" + method; pluginConfigMap.put(serviceMethod, pluginService); } } logger.info("pluginConfigMap initialized ..."); } private void initPluginConfigMap() { if (initStatus == false) { synchronized (initStatus) { if (initStatus == false) { loadPluginService(); initStatus = true; } } } } }
当然还有最重要的插件实现类:
@PluginPoint(service = UserService.class, method = "getUserById", type=PluginType.AFTER) public class UserServicePlugin implements PluginService { @Override public Object process(Object[] args, Object result) throws Exception { User user = (User) result; // 增加脱敏的业务逻辑 desensitive(user); return user; } private void desensitive(User user) { //TODO ... } }
至此我们的工作基本就算完成了,启动项目时只要UserServicePlugin被加载到,那么再次调用getUserById方法时,返回的就是脱敏后的人员信息了。
一点总结:
软件开发过程中需求难免会发生变化,今日精巧的设计,明天就可能变成拦路的淤泥。