Mybatis动态数据源 背景 有时候需要执行一些复杂的SQL,需要用到Mybatis,但是数据源又是变化的,这个时候就要支持动态数据源。 public SqlSession getSqlSession(String id) { DataSource dataSource = getDataSource(id); try { SqlSessionFactory sqlSessionFactory = sqlSessionFactory(dataSource); SqlSession sqlSession = sqlSessionFactory.openSession(); return sqlSession; } catch (Exception e) { log.error("getSqlSession failed.", e); throw new GlobalException(RespEnum.DATASOURCE_GET_SESSION_FAILED.getMsg()); } } public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception { SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean(); Configuration configuration = new Configuration(); configuration.setJdbcTypeForNull(JdbcType.VARCHAR); configuration.setCallSettersOnNulls(true); factoryBean.setConfiguration(configuration); //设置mapper factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*.xml")); //设置数据源 factoryBean.setDataSource(dataSource); return factoryBean.getObject(); } 测试 SqlSession sqlSession = dataSourceManager.getSqlSession(datasourceId); CommonDao commonDao = sqlSession.getMapper(CommonDao.class);
Spring Bean注入或替换
Spring Bean注入或替换 1、ApplicationContext 一个比较另类的方法,反射出内部容器Map进行替换,不建议使用。 @Component public class ApplicationContextUtil implements ApplicationContextAware { private String packageName = "com"; private static ApplicationContext applicationContext = null; @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { ApplicationContextUtil.applicationContext = applicationContext; Reflections f = new Reflections(packageName); Set<Class<?>> set = f.getTypesAnnotatedWith(Config.class); for (Class<?> clazz : set) { try { Object proxy = ProxyBeanFactory.getProxy(clazz); String beanName = StringUtils.uncapitalize(clazz.getSimpleName()); replaceBean(beanName, proxy); } catch (Exception e) { throw new RuntimeException(e); } } } public static ApplicationContext getApplicationContext() { return applicationContext; } public static <T> T getBean(Class<T> clazz) { return getApplicationContext().getBean(clazz); } //替换bean public static void replaceBean(String beanName, Object targetObj) throws NoSuchFieldException, IllegalAccessException { ConfigurableApplicationContext context = (ConfigurableApplicationContext) getApplicationContext(); DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) context.getBeanFactory(); //反射获取Factory中的singletonObjects 将该名称下的bean进行替换 Field singletonObjects = DefaultSingletonBeanRegistry.class.getDeclaredField("singletonObjects"); singletonObjects.setAccessible(true); Map<String, Object> map = (Map<String, Object>) singletonObjects.get(beanFactory); map.put(beanName, targetObj); } } 2、BeanDefinitionRegistry 实现 BeanDefinitionRegistryPostProcessor 获取 BeanDefinitionRegistry,可以对Bean进行处理。 ...
Spring Bean生命周期与扩展点
Spring生命周期与扩展点 1、生命周期 2、扩展点 参考:https://segmentfault.com/a/1190000023033670
代理对象注入
代理对象注入 定制配置的获取中,我们可以通过对一个配置的Bean进行代理,使他能够自动地切换客户来获取配置,对用户屏蔽具体的实现细节。我们要实现对一个有@Config注解的Bean对象进行增强,然后将代理Bean注入Spring容器中。配置Bean示例代码如下。 具体使用方法参考:https://kb.cvte.com/pages/viewpage.action?pageId=320163817。 @Data @Config(prefix = "email-server", separator = ".") public class EmailServer { private String host = "email-smtp.eu-west-1.amazonaws.com"; private Integer port = 465; private String from = "noreply@bytello.com"; private String nickName = "Bytello"; /** * email-server.account.username */ @ConfigKey("account.username") private String username = "bytello"; /** * email-server.account.password */ @ConfigKey("account.password") private String password = "BDrm6MyBBr/hj"; } 方法一 Spring AOP 采用了AOP拦截基础包的所有方法,并对这个方法所在的类进行判断,判断这个类有没有@Config注解,有就进行增强处理。 拦截器 MethodInterceptor public class ConfigAnnotationInterceptor implements MethodInterceptor { private CustomizerConfig customizerConfig; public ConfigAnnotationInterceptor(){ this.customizerConfig = SpringUtil.getBean(CustomizerConfig.class); } @Override public Object invoke(MethodInvocation methodInvocation) throws Throwable { //判断是否有Config注解,没有就直接方法执行结果(默认值) Object defaultValue = methodInvocation.proceed(); Config annotation = methodInvocation.getMethod().getDeclaringClass().getAnnotation(Config.class); if (annotation == null) { return defaultValue; } //获取方法名,截取getXxx的属性值xxx String key = methodInvocation.getMethod().getName().substring(3); String lowercaseKey = StringUtils.uncapitalize(key); //查出值并做类型转化 Object value = customizerConfig.getProperty(lowercaseKey, defaultValue); Object res = new ObjectMapper().convertValue(value, methodInvocation.getMethod().getReturnType()); return res; } } 配置类 ...
审计日志注解
审计日志注解 1、针对增删改接口进行可视化日志审查 2、过滤敏感字段 package com.bytello.customizer.aspect; import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.support.spring.PropertyPreFilters; import com.bytello.customizer.common.annotation.Log; import com.bytello.customizer.common.enums.Op; import com.bytello.customizer.common.pojo.entity.SysOpLogDo; import com.bytello.customizer.common.pojo.entity.UserDo; import com.bytello.customizer.common.utils.FastJsonUtil; import com.bytello.customizer.common.utils.ServletUtil; import com.bytello.customizer.holder.UserHolder; import com.bytello.customizer.manager.AsyncManager; import com.bytello.customizer.manager.factory.AsyncFactory; import org.apache.commons.lang3.StringUtils; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.AfterReturning; import org.aspectj.lang.annotation.AfterThrowing; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import org.springframework.validation.BindingResult; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import org.springframework.web.multipart.MultipartFile; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.Collection; import java.util.Date; import java.util.HashMap; import java.util.Map; /** * 操作日志记录 * * @author wuqinhong * @email wuqinhong@cvte.com * @date 2022-11-14 09:34:45 */ @Aspect @Component public class LogAspect { private static final Logger log = LoggerFactory.getLogger(LogAspect.class); /** * 排除敏感属性字段 */ public static final String[] EXCLUDE_PROPERTIES = {"password", "oldPassword", "newPassword", "confirmPassword"}; // 配置织入点 @Pointcut("@annotation(com.bytello.customizer.common.annotation.Log)") public void logPointCut() { } /** * 处理完请求后执行 * * @param joinPoint 切点 */ @AfterReturning(pointcut = "@annotation(controllerLog)", returning = "jsonResult") public void doAfterReturning(JoinPoint joinPoint, Log controllerLog, Object jsonResult) { handleLog(joinPoint, controllerLog, null, jsonResult); } /** * 拦截异常操作 * * @param joinPoint 切点 * @param e 异常 */ @AfterThrowing(value = "@annotation(controllerLog)", throwing = "e") public void doAfterThrowing(JoinPoint joinPoint, Log controllerLog, Exception e) { handleLog(joinPoint, controllerLog, e, null); } protected void handleLog(final JoinPoint joinPoint, Log controllerLog, final Exception e, Object jsonResult) { try { // 获取当前的用户 UserDo currentUser = UserHolder.get(); // *========数据库日志=========*// SysOpLogDo opLog = new SysOpLogDo(); opLog.setStatus(Op.Status.SUCCESS.ordinal()); // 请求的地址 HttpServletRequest req = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); String ip = req.getRemoteAddr(); opLog.setOpIp(ip); // 操作时间 opLog.setOpTime(new Date()); // 请求地址 opLog.setOpUrl(StringUtils.substring(ServletUtil.getRequest().getRequestURI(), 0, 255)); if (currentUser != null) { opLog.setOpName(currentUser.getAccount()); } // 获取异常信息 if (e != null) { opLog.setStatus(Op.Status.FAIL.ordinal()); opLog.setErrorMsg(StringUtils.substring(e.getMessage(), 0, 2000)); } // 设置方法名称 String className = joinPoint.getTarget().getClass().getName(); String methodName = joinPoint.getSignature().getName(); opLog.setMethod(className + "." + methodName + "()"); // 设置请求方式 opLog.setRequestMethod(ServletUtil.getRequest().getMethod()); // 处理设置注解上的参数 getControllerMethodDescription(joinPoint, controllerLog, opLog, jsonResult); // 保存数据库 AsyncManager.instance().execute(AsyncFactory.recordOp(opLog)); } catch (Exception exp) { // 记录本地异常日志 log.error("==前置通知异常=="); log.error("异常信息:{}", exp.getMessage()); exp.printStackTrace(); } } /** * 获取注解中对方法的描述信息 用于Controller层注解 * * @param log 日志 * @param opLog 操作日志 * @throws Exception */ public void getControllerMethodDescription(JoinPoint joinPoint, Log log, SysOpLogDo opLog, Object jsonResult) throws Exception { // 设置标题 opLog.setTitle(log.title()); //设置操作类型 opLog.setOpType(log.opType().ordinal()); // 是否需要保存request,参数和值 if (log.isSaveRequestData()) { // 获取参数的信息,传入到数据库中。 //setRequestValue(joinPoint, opLog); setRequestBody(opLog); } // 是否需要保存response,参数和值 if (log.isSaveResponseData() && jsonResult != null) { opLog.setJsonResult(StringUtils.substring(JSONObject.toJSONString(jsonResult), 0, 2000)); } } /** * 获取请求体,放到log中 * * @param opLog */ private void setRequestBody(SysOpLogDo opLog){ String requestJson = ServletUtil.getRequestBody(); //过滤敏感属性 HashMap<String, String> map = FastJsonUtil.json2Map(requestJson); String filtered = JSONObject.toJSONString(map, excludePropertyPreFilter()); opLog.setOpParam(filtered); } /** * 忽略敏感属性 */ public PropertyPreFilters.MySimplePropertyPreFilter excludePropertyPreFilter() { return new PropertyPreFilters().addFilter().addExcludes(EXCLUDE_PROPERTIES); } }
幂等注解优化
幂等注解优化 1、代码对比 优化前 @Override public ConsumerStatus onMessage(Message msg) { String key = msg.getKeys(); String jsonStr = MessageHelper.toString(msg.getBody()); log.info("getMsg:{}", jsonStr); //保证消费幂等 if (redisService.get(key) != null) { log.warn("The message whose key is {} has been consumed, will ignore!", key); return ConsumerStatus.SUCCESS; } try { AccountRebindEmailVo accountRebindEmailVo = JSON.parseObject(jsonStr, AccountRebindEmailVo.class); coursewareShareMapper.updateReceiverPhoneByReceiverId(accountRebindEmailVo.getUid(), accountRebindEmailVo.getEmail()); try { redisService.put(key, expired, ""); } catch (Exception ex) { //ignore } return ConsumerStatus.SUCCESS; } catch (Exception ex) { log.error("AccountPlatformSubscriber consume fail:{}", ex.toString()); return ConsumerStatus.FAIL; } } 优化后 @Idempotent(value = "#msg.getUniqKey()", expireTime = 5, timeUnit = TimeUnit.HOURS) @Override public ConsumerStatus onMessage(Message msg) { String jsonStr = MessageHelper.toString(msg.getBody()); log.info("getMsg:{}", jsonStr); AccountRebindEmailVo accountRebindEmailVo = JSON.parseObject(jsonStr, AccountRebindEmailVo.class); coursewareShareMapper.updateReceiverPhoneByReceiverId(accountRebindEmailVo.getUid(), accountRebindEmailVo.getEmail()); return ConsumerStatus.SUCCESS; } 2、实现 幂等注解 @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Idempotent { String value(); int expireTime() default 60; TimeUnit timeUnit() default TimeUnit.SECONDS; String info() default "重复执行,将中止执行"; } 切面处理:IdempotentAspect /** * @description: 幂等注解切面 * @author: wuqinhong * @date: 2022/11/4 **/ @Slf4j @Aspect @Component public class IdempotentAspect { @Autowired private BaseRedisService redisService; /** * spel */ private ExpressionParser parser = new SpelExpressionParser(); /** * spring下获取参数名的组件 */ private LocalVariableTableParameterNameDiscoverer discoverer = new LocalVariableTableParameterNameDiscoverer(); @Around("@annotation(com.encloud.annotation.Idempotent)") public void handle(ProceedingJoinPoint joinPoint) throws Throwable { //获取注解的属性值 Idempotent idempotent = getDeclaredAnnotation(joinPoint); String exp = idempotent.value(); int expireTime = idempotent.expireTime(); TimeUnit timeUnit = idempotent.timeUnit(); int seconds = (int) timeUnit.toSeconds(expireTime); String info = idempotent.info(); //根据sqel表达式获取值 Object[] args = joinPoint.getArgs(); Method method = getMethod(joinPoint); EvaluationContext context = this.bindParam(method, args); Expression expression = parser.parseExpression(exp); String key = (String) expression.getValue(context); //redis检查是否存在key if (redisService.get(key) != null) { log.warn(info); return; } //执行切点方法 joinPoint.proceed(); //redis设置key redisService.put(key, seconds, null); } /** * 获取方法中声明的注解 * * @param joinPoint * @return * @throws NoSuchMethodException */ public Idempotent getDeclaredAnnotation(JoinPoint joinPoint) throws NoSuchMethodException { // 获取方法名 String methodName = joinPoint.getSignature().getName(); // 反射获取目标类 Class<?> targetClass = joinPoint.getTarget().getClass(); // 拿到方法对应的参数类型 Class<?>[] parameterTypes = ((MethodSignature) joinPoint.getSignature()).getParameterTypes(); // 根据类、方法、参数类型(重载)获取到方法的具体信息 Method objMethod = targetClass.getMethod(methodName, parameterTypes); // 拿到方法定义的注解信息 Idempotent annotation = objMethod.getDeclaredAnnotation(Idempotent.class); // 返回 return annotation; } /** * 获取当前执行的方法 * * @param pjp * @return * @throws NoSuchMethodException */ private Method getMethod(ProceedingJoinPoint pjp) throws NoSuchMethodException { MethodSignature methodSignature = (MethodSignature) pjp.getSignature(); Method method = methodSignature.getMethod(); Method targetMethod = pjp.getTarget().getClass().getMethod(method.getName(), method.getParameterTypes()); return targetMethod; } /** * 将方法的参数名和参数值绑定 * * @param method 方法,根据方法获取参数名 * @param args 方法的参数值 * @return */ private EvaluationContext bindParam(Method method, Object[] args) { //获取方法的参数名 String[] params = discoverer.getParameterNames(method); //将参数名与参数值对应起来 EvaluationContext context = new StandardEvaluationContext(); for (int len = 0; len < params.length; len++) { context.setVariable(params[len], args[len]); } return context; } } 3、技术点 Spel表达式 ...
异步请求优化
请求异步编排优化 1、优化前 @Override public AccountProductUseInfo getProductByAccount(String uid) throws ExecutionException, InterruptedException { //请求数据 int classCount = classApiService.getTotalCount(uid); int activityGameCount = activityGameApiService.getActivityGameCount(uid); int coursewareCount = coursewareApi.getCoursewareCountByUid(uid); DriveUsedBaseInfoDto spaceDetail = driveApi.getPersonalSpaceDetail(uid); Double driveSpaceUsed = spaceDetail.getUsed().doubleValue() / 1024 / 1024; String driveSpaceUsedFormat = String.format("%.2fMB", driveSpaceUsed); List<String> appCodes = userMapper.getAppCodesByUid(uid); //构建结果集 AccountProductUseInfo productUseInfo = new AccountProductUseInfo(); productUseInfo.setAppCodes(appCodes); productUseInfo.setClassCount(classCount); productUseInfo.setActivityGameCount(activityGameCount); productUseInfo.setCoursewareCount(coursewareCount); productUseInfo.setDriveSpaceUsed(driveSpaceUsedFormat); return productUseInfo; } 结果:维持在2s~3s 2、优化后 @Override public AccountProductUseInfo getProductByAccount(String uid) { //异步请求数据 CompletableFuture<Integer> classFuture = CompletableFuture.supplyAsync(() -> classApiService.getTotalCount(uid), syncRequestThreadPool); CompletableFuture<Integer> activityGameFuture = CompletableFuture.supplyAsync(() -> activityGameApiService.getActivityGameCount(uid), syncRequestThreadPool); CompletableFuture<Integer> coursewareFuture = CompletableFuture.supplyAsync(() -> coursewareApi.getCoursewareCountByUid(uid), syncRequestThreadPool); CompletableFuture<DriveUsedBaseInfoDto> spaceDetailFuture = CompletableFuture.supplyAsync(() -> driveApi.getPersonalSpaceDetail(uid), syncRequestThreadPool); CompletableFuture<List<String>> appCodesFuture = CompletableFuture.supplyAsync(() -> userMapper.getAppCodesByUid(uid), syncRequestThreadPool); //等待所有请求完成 CompletableFuture<Void> allFuture = CompletableFuture.allOf(classFuture, activityGameFuture, coursewareFuture, appCodesFuture); Integer classCount; Integer activityGameCount; Integer coursewareCount; List<String> appCodes; DriveUsedBaseInfoDto spaceDetail; try { allFuture.get(5, TimeUnit.SECONDS); classCount = classFuture.join(); activityGameCount = activityGameFuture.join(); coursewareCount = coursewareFuture.join(); appCodes = appCodesFuture.join(); spaceDetail = spaceDetailFuture.join(); } catch (Exception e) { log.error("Async Request Error:", e); throw BusinessExceptionUtils.getBusinessException(ApplicationEnum.ASYNC_REQUEST_ERROR); } //构建结果集 AccountProductUseInfo productUseInfo = new AccountProductUseInfo(); Double driveSpaceUsed = spaceDetail.getUsed().doubleValue() / 1024 / 1024; String driveSpaceUsedFormat = String.format("%.2fMB", driveSpaceUsed); productUseInfo.setAppCodes(appCodes); productUseInfo.setClassCount(classCount); productUseInfo.setActivityGameCount(activityGameCount); productUseInfo.setCoursewareCount(coursewareCount); productUseInfo.setDriveSpaceUsed(driveSpaceUsedFormat); return productUseInfo; } 结果:维持在100ms到300ms ...
日志打印重构
项目脚手架——日志打印重构 重构前 日志记录使用Spring Interceptor拦截器来统一记录,我们的预期是打印出请求的详细信息,包括 traceId,请求头,请求体,响应头,响应体。 使用 ThreadLocal 帮助保存响应信息和请求开始时间,方便对请求时间进行统计。 使用请求包装类来包装HttpServletRequest,因为请求内容是以输入流的形式存在,只能读取一次,所以使用包装类来复制流的内容并存到字节数组中。 问题: 1、如果请求在到达记录日志的Interceptor之前发生了错误,将直接返回且不会记录日志。比如请求在验证权限的 LoginInterceptor 被拦截,将不会达到记录日志的 LogInterceptor。这对问题排查造成了一定的阻碍。 2、使用比较多的 ThreadLocal,使得代码有些耦合,不够优雅。 重构 1、将日志打印放到 Filter 来做,避免不会记录日志的问题。 2、使用装饰器设计模式,对 HttpServletRequest 和 HttpServletResponse 进行扩展,使得我们可以多次获取到请求内容和响应内容,并且不需要使用ThreadLocal,日志相关的代码不会放到其他类中,遵循类的单一职责原则。 3、Filter不能设置排除路径,自定义 ExcludePathFilter 类对 GenericFilterBean 进行包装增强,不需要每次在 doFilter 方法内判断路径并跳过。
本地内存排查工具使用
cat /proc/$1/maps | grep -Fv ".so" | grep " 0 " | awk '{print $1}' | grep $2 | ( IFS="-" while read a b; do dd if=/proc/$1/mem bs=$( getconf PAGESIZE ) iflag=skip_bytes,count_bytes \ skip=$(( 0x$a )) count=$(( 0x$b - 0x$a )) of="$1_mem_$a.bin" done )
统一登录定制部分代码重构
统一登录接入定制重构 重构前 代码结构 BrandStrategy public interface BrandStrategy { /** * 获取品牌标识 * * @return */ String getBrand(); /** * 获取动态码邮件信息 * * @return */ DynamicCodeEmailInfoEntity getDynamicCodeEmailInfo(); /** * 获取激活邮件信息 * * @return */ MailTemplateVo getActivationMail(); /** * 获取邮箱服务器配置 * * @return */ EmailServerInfoVo getEmailServerInfoVo(); } BytelloStrategy @Slf4j @Component public class BytelloStrategy implements BrandStrategy{ @Autowired MailConfig mailConfig; @Value("${spring.mail.username}") private String mailUsername; @Value("${ifp.mail.from:null}") private String from; @Value("${spring.mail.password}") private String mailPassword; @Value("${spring.mail.host}") private String mailHost; @Value("${spring.mail.port}") private Integer mailPort; @Value("${spring.mail.protocol}") private String mailProtocol; @Value("${ifp.mail.nickname}") private String mailNickname; @Override public String getBrand() { return BrandEnum.BYTELLO.getBrand(); } @Override public DynamicCodeEmailInfoEntity getDynamicCodeEmailInfo() { DynamicCodeEmailInfoEntity dynamicCodeEmailInfoEntity = new DynamicCodeEmailInfoEntity(); dynamicCodeEmailInfoEntity.setSubjectCode("Bytello.email.en.registerDynamic.title"); dynamicCodeEmailInfoEntity.setContentCode("Bytello.email.en.registerDynamic.content"); return dynamicCodeEmailInfoEntity; } @Override public MailTemplateVo getActivationMail() { MailTemplateVo mailTemplateVo = new MailTemplateVo(); mailTemplateVo.setMailSubject(mailConfig.getBytelloActivationSubEnglish()); mailTemplateVo.setMailContent(mailConfig.getBytelloActivationContentEnglish()); return mailTemplateVo; } @Override public EmailServerInfoVo getEmailServerInfoVo() { EmailServerInfoVo emailServerInfoVo = new EmailServerInfoVo(); emailServerInfoVo.setAccount(mailUsername); emailServerInfoVo.setPassword(mailPassword); emailServerInfoVo.setServerPort(mailPort); emailServerInfoVo.setServerHost(mailHost); emailServerInfoVo.setProtocol(mailProtocol); emailServerInfoVo.setSendName(mailNickname); emailServerInfoVo.setSendAccount(from); return emailServerInfoVo; } } DeephubStrategy ...