008 — Sentinel очищает RESTful @PathVariable

Spring Boot Микросервисы

Это 8-я статья по плану технического написания (включая перевод), с небольшой целью 999, не менее 2 статей в неделю.

Некоторое время назад большинство статей было об эксплуатации и обслуживании, в последнее время вышла волна вопросов, связанных с бэкендом.

задний план

Недавно я начал использовать Sentinel для защиты трафика, но фильтр веб-сервлетов по умолчанию перехватывает все HTTP-запросы. В традиционных проектах особых проблем нет. Но если в проекте используется Spring MVC и используется @PathVariable, будет неловко.
Например, шаблон uri/foo/{id}, и от мониторинга Sentinel/foo/1 а также/foo/2Ресурсов всего два, а Sentinel поддерживает максимум 6000 ресурсов, и он не вступит в силу, если их больше.

Решение

Официальное решение: UrlCleaner

 WebCallbackManager.setUrlCleaner(new UrlCleaner() {
            @Override
            public String clean(String originUrl) {
                if (originUrl.startsWith(fooPrefix)) {
                    return "/foo/*";
                }
                return originUrl;
            }
        });

Но меня тошнит от одной мысли об этом,/v1/{foo}/{bar}/qux/{baz}Таких около 20, и я посмотрю на один.

AOP

Другой способ мышления: шаблон uri сложно сделать, используйте глупый способ доступа к головному офису, верно? Ответ положительный.

@Aspect
public class SentinelResourceAspect {
    @Pointcut("within(com.anjia.*.web.rest..*)")
    public void sentinelResourcePackagePointcut() {
        // Method is empty as this is just a Pointcut, the implementations are
        // in the advices.
    }
    @Around("sentinelResourcePackagePointcut()")
    public Object sentinelResourceAround(ProceedingJoinPoint joinPoint) throws Throwable {
        Entry entry = null;
        // 务必保证finally会被执行
        try {
          // 资源名可使用任意有业务语义的字符串
          // 注意此处只是类名#方法名,方法重载是合并的,如果需要进行区分,
          // 可以获取参数类型加入到资源名称上
          entry = SphU.entry(joinPoint.getSignature().getDeclaringTypeName()+
                             "#"+joinPoint.getSignature().getName());
          // 被保护的业务逻辑
          // do something...
        } catch (BlockException ex) {
          // 资源访问阻止,被限流或被降级
          // 进行相应的处理操作
        } finally {
          if (entry != null) {
            entry.exit();
          }
        }
        return result;
    }
}

перехватчик

Просмотрите процесс выполнения Spring mvcdoFilter -> doService -> dispatcher -> preHandle -> controller -> postHandle -> afterCompletion -> filterAfter

ЯдроString pattern = (String) request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE);Однако он назначается только на этапе диспетчера, поэтому недоступен в CommFilter, поэтому использовать официальный Фильтр невозможно. только перехватчик


import com.alibaba.csp.sentinel.EntryType;
import com.alibaba.csp.sentinel.SphU;
import com.alibaba.csp.sentinel.adapter.servlet.callback.RequestOriginParser;
import com.alibaba.csp.sentinel.adapter.servlet.callback.UrlCleaner;
import com.alibaba.csp.sentinel.adapter.servlet.callback.WebCallbackManager;
import com.alibaba.csp.sentinel.adapter.servlet.util.FilterUtil;
import com.alibaba.csp.sentinel.context.ContextUtil;
import com.alibaba.csp.sentinel.log.RecordLog;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.csp.sentinel.util.StringUtil;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Component
public class SentinelHandlerInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String origin = parseOrigin(request);
        String pattern = (String) request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE);
        String uriTarget = StringUtils.defaultString(pattern,FilterUtil.filterTarget(request));
        try {
            // Clean and unify the URL.
            // For REST APIs, you have to clean the URL (e.g. `/foo/1` and `/foo/2` -> `/foo/:id`), or
            // the amount of context and resources will exceed the threshold.
            UrlCleaner urlCleaner = WebCallbackManager.getUrlCleaner();
            if (urlCleaner != null) {
                uriTarget = urlCleaner.clean(uriTarget);
            }
            RecordLog.info(String.format("[Sentinel Pre Filter] Origin: %s enter Uri Path: %s", origin, uriTarget));
            SphU.entry(uriTarget, EntryType.IN);
            return true;
        } catch (BlockException ex) {
            RecordLog.warn(String.format("[Sentinel Pre Filter] Block Exception when Origin: %s enter fall back uri: %s", origin, uriTarget), ex);
            WebCallbackManager.getUrlBlockHandler().blocked(request, response, ex);
            return false;
        }
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        while (ContextUtil.getContext() != null && ContextUtil.getContext().getCurEntry() != null) {
            ContextUtil.getContext().getCurEntry().exit();
        }
        ContextUtil.exit();
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

    }

    private String parseOrigin(HttpServletRequest request) {
        RequestOriginParser originParser = WebCallbackManager.getRequestOriginParser();
        String origin = EMPTY_ORIGIN;
        if (originParser != null) {
            origin = originParser.parseOrigin(request);
            if (StringUtil.isEmpty(origin)) {
                return EMPTY_ORIGIN;
            }
        }
        return origin;
    }


    private static final String EMPTY_ORIGIN = "";
}


import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;

@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
    @Inject
    SentinelHandlerInterceptor sentinelHandlerInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(sentinelHandlerInterceptor);
    }
}

UrlBlockHandler и UrlCleaner и WebServletConfig.setBlockPage(blockPage)

Как упоминалось выше, UrlCleaner используется для объединения запросов и очистки URL-адресов. А UrlBlockHandler является обработчиком по умолчанию после перехвата. Но clean и обработчик не связаны между собой, так что если есть несколько обработок, вам нужно делать логические суждения в одном методе.

UrlCleaner

 WebCallbackManager.setUrlCleaner(new UrlCleaner() {
            @Override
            public String clean(String originUrl) {
                if (originUrl.startsWith(fooPrefix)) {
                    return "/foo/*";
                }
                return originUrl;
            }
        });

UrlBlockHandler
Если это более общее, вы можете адаптивно возвращать контент (PLAN_TEXT и JSON) в соответствии с типом контента запроса.

WebCallbackManager.setUrlBlockHandler((request, response, ex) -> {
    response.addHeader("Content-Type","application/json;charset=UTF-8");
    PrintWriter out = response.getWriter();
    out.print("{\"code\"":429,\"msg\":\"系统繁忙,请稍后重试\""}");
    out.flush();
    out.close();
});

WebServletConfig.setBlockPage(blockPage)

WebServletConfig.setBlockPage("http://www.baidu.com")

Обратите внимание, что ни один из трех методов не поддерживает цепочки вызовов, например, я написал два UrlBlockHandler и распознал только последний.

использованная литература

Небольшое объявление о приеме на работу

Небольшие партнеры в Цзинане, Шаньдун могут представить резюмеПрисоединяйтесь к нам, делать вещи вместе.

Долгосрочный набор, Java-программист, инженер по большим данным, инженер по эксплуатации и техническому обслуживанию, фронтенд-инженер.