Assistente de economia de fluxo
O motivo da falha do HttpMessageConverter é: um interceptor é usado no projeto para interceptar a solicitação, e algumas interfaces precisam estar logadas para acessar, caso contrário, uma resposta no formato texto/html é retornada, o que faz com que o serviço remoto falhe ao analisar a resposta.
O motivo da falha de login é: quando Feign inicia uma chamada remota, ele irá regenerar uma nova solicitação. O problema é que ele não carregará o cookie da solicitação original, resultando em falha ao chamar a interface remota que precisa fazer login . A solução é configurar um interceptador Feign e trazer o cookie da requisição original no momento do envio da requisição.
O conteúdo principal deste artigo é uma série de pontos de conhecimento sobre este assunto, incluindo, mas não se limitando a:
- tipo de conteúdo http
- Depuração conjunta de microsserviços
- Ver registros do Feign
- interceptador de login
- Fegin perdeu problema de cabeça
Análise e Posicionamento de Problemas
Hoje, ao depurar conjuntamente dois microsserviços, descobri que a interface remota sempre retornava o seguinte erro:
Could not extract response: no suitable HttpMessageConverter found for response type [class top.dumbzarro.greensource.common.utils.R] and content type [text/html;charset=UTF-8]
Significa que não há HttpMessageConverter que possa converter [text/html;charset=UTF-8] em [class top.dumbzarro.greensource.common.utils.R].
Entre eles, R é um objeto de retorno comum definido no projeto, e todas as interfaces retornam esse objeto.
A interface remota é servida em software, os detalhes são os seguintes:
@FeignClient("greensource-member")
public interface MemberFeignService {
@GetMapping("/memberreceiveaddress/info/{id}")
R info(@PathVariable("id") Long id);
}
A interface chamada está no serviço membro, os detalhes são os seguintes:
@RestController
@RequestMapping("memberreceiveaddress")
public class MemberReceiveAddressController {
@Autowired
private MemberReceiveAddressService memberReceiveAddressService;
@GetMapping("/info/{id}")
//@RequiresPermissions("member:memberreceiveaddress:info")
public R info(@PathVariable("id") Long id){
MemberReceiveAddressEntity memberReceiveAddress = memberReceiveAddressService.getById(id);
return R.ok().setData(memberReceiveAddress);
}
}
O que é confuso é que antes da depuração conjunta desses dois serviços, o serviço de autenticação e o serviço de membro, o serviço de autenticação e o serviço de terceiros foram ajustados, e não há problema com a chamada remota Feign entre os dois serviços.
A solução online para nenhum HttpMessageConverter adequado é adicionar um conversor personalizado e assim por diante. Mas sinto vagamente que este não é um problema de conversão de tipo, caso contrário o serviço anterior não seria capaz de funcionar sem configuração adicional.
Tipo de conteúdo HTTP
Content-type é um campo no protocolo HTTP. O cabeçalho Content-Type informa ao cliente o tipo de conteúdo real retornado.
Os mais comuns são:
- text/html : Formato HTML. Quando o navegador obtiver este tipo de arquivo, ele chamará automaticamente o analisador HTML para renderizar o arquivo.
- text/plain : Defina o arquivo como texto simples e o navegador não o processará quando obtiver esse arquivo.
- application/json : formato de dados JSON, que não será processado pelo navegador.
TODO Tipo de conteúdo springmvc tipo de conteúdo padrão do fegin
Na minha impressão, todas as interfaces retornam dados json, e o tipo de conteúdo é application/json. Como é que text/html aparece de repente? Então usei a pesquisa global para verificar.
De repente, ocorreu-me que um interceptador foi adicionado a alguns serviços que exigem login para determinar se o usuário está logado. Quando for determinado que o usuário não está logado, uma resposta de texto/html será retornada. O código detalhado é o seguinte.
@Component
public class LoginUserInterceptor implements HandlerInterceptor {
public static ThreadLocal<MemberResponseVo> loginUser = new ThreadLocal<>();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String uri = request.getRequestURI();
if(uri.equals("/error")){
response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
out.println("<script>alert('uri为 /error, 可能原因为:1.请求方法错误 2.参数格式解析错误');</script>");
return false;
}
boolean match = new AntPathMatcher().match("/member/**", uri);
if (match) {
// member接口(登陆,注册)可以不用登陆就使用,否则需要登陆
return true;
}
HttpSession session = request.getSession();
//获取登录的用户信息
MemberResponseVo attribute = (MemberResponseVo) session.getAttribute(LOGIN_USER);
if (attribute != null) {
//把登录后用户的信息放在ThreadLocal里面进行保存
loginUser.set(attribute);
return true;
} else {
//未登录,返回登录页面
response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
out.println("<script>alert('请先进行登录,再进行后续操作!');location.href='http://auth.dumbzarro.top/login.html'</script>");
return false;
}
}
No momento da solicitação de Feign, foi julgado que ele não estava logado, então os dados no formato "text/html" foram retornados, e usamos R para aceitá-los na interface remota, então naturalmente não puderam ser analisados com sucesso e um erro apareceria.
Normalmente, o que deve ser retornado aqui é um objeto de aplicação. Como este projeto é modificado com base no Guli Mall, os front-ends e back-ends do Guli Mall não são separados, e o projeto subsequente utiliza uma estrutura que separa os front-ends e back-ends , então aqui está este erro que pode ser resolvido modificando o valor de retorno.
Você pode consultar as seguintes modificações de código
@Component
public class LoginUserInterceptor implements HandlerInterceptor {
public static ThreadLocal<MemberResponseVo> loginUser = new ThreadLocal<>();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String uri = request.getRequestURI();
if(uri.equals("/error")){
response.setContentType("application/json; charset=utf-8");
PrintWriter out = response.getWriter();
out.println(JSONObject.toJSONString(R.error()
.put("error","uri为 /error, 可能原因为:1.请求方法错误 2.参数格式解析错误"),
SerializerFeature.WriteMapNullValue,
SerializerFeature.WriteDateUseDateFormat));
return false;
}
boolean match = new AntPathMatcher().match("/member/**", uri);
if (match) {
// member接口(登陆,注册)可以不用登陆就使用,否则需要登陆
return true;
}
HttpSession session = request.getSession();
//获取登录的用户信息
MemberResponseVo attribute = (MemberResponseVo) session.getAttribute(LOGIN_USER);
if (attribute != null) {
//把登录后用户的信息放在ThreadLocal里面进行保存
loginUser.set(attribute);
return true;
} else {
//未登录
response.setContentType("application/json; charset=utf-8");
PrintWriter out = response.getWriter();
out.println(JSONObject.toJSONString(
R.error().put("error","用户未登录"),
SerializerFeature.WriteMapNullValue,
SerializerFeature.WriteDateUseDateFormat
)
);
return false;
}
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
}
usuário não logado
Embora nenhuma exceção de conversão seja informada, "o usuário não está logado" será retornado.
O que pode ser garantido é que já loguei no swagger, e trouxe um cookie ao solicitar, mas depois do fegin, mostra que não loguei, mas apenas a interface do serviço ware reporta um erro, e nem auth nem terceiros reportarão um erro.
Depuração conjunta de microsserviços
Como não há problema ao testar apenas o serviço de membro, quero ver a diferença entre solicitar diretamente o serviço de membro e solicitar o membro do servidor ware, então pretendo interromper ambos os serviços. Observe que se você tiver várias instâncias do mesmo serviço registradas no nacos, você deve adicionar o parâmetro url a @FeignClient para especificar o serviço local, caso contrário a solicitação poderá ser enviada para outras máquinas, resultando em nenhuma maneira de depurar para o atual em a máquina. Claro, se houver apenas uma instância, você não precisará adicioná-la. Os exemplos são os seguintes:
//@FeignClient(value="greensource-member")
@FeignClient(value="greensource-member",url="localhost:7000")// 指定某台机器
public interface MemberFeignService {
@GetMapping("/memberreceiveaddress/info/{id}")
R info(@PathVariable("id") Long id);
}
Neste momento, inicie o serviço, inicie a depuração e descubra que o programa não passa pela chamada da interface, mas é considerado como não logado no interceptador de login do membro e retorna diretamente ao serviço de ware.
Verifique a solicitação e descubra que não há sessão no momento e o login não foi bem-sucedido.
abrir log fegin
Configuramos um FeginConfig para visualizar a resposta da solicitação do fegin
@Configuration
public class FeignConfig {
@Bean
public feign.Logger logger() {
return new Slf4jLogger();
}
@Bean
public Logger.Level level() {
return Logger.Level.FULL;
}
}
Configure o log de impressão em application.yml
logging:
level:
feign.Logger: debug
log4j define 8 níveis de log, a prioridade de alta para baixa é: OFF, FATAL, ERROR, WARN, INFO, DEBUG, TRACE, ALL. A prioridade padrão do log4j é ERRO. Log4j recomenda usar apenas os quatro níveis de ERROR, WARN, INFO e DEBUG (prioridade de alta para baixa). Se o nível de registro estiver definido para um determinado nível, os registros com prioridade mais alta que esse nível poderão ser impressos.
- ALL : O nível mais baixo, usado para ativar todos os registros.
- TRACE : Nível de log muito baixo, geralmente não usado.
- DEBUG : Ressalta que eventos de informações refinadas são muito úteis para depuração de aplicativos e são usados principalmente para imprimir algumas informações em execução durante o processo de desenvolvimento.
- INFO : As mensagens destacam o progresso do aplicativo em um nível granular. Imprima algumas informações de seu interesse ou importantes.Isso pode ser usado para gerar algumas informações importantes sobre a execução do programa no ambiente de produção, mas não pode ser abusado para evitar a impressão de muitos logs.
- AVISO : Indica que uma possível situação de erro ocorrerá. Algumas informações não são informações de erro, mas também fornecem algumas dicas ao programador.
- ERRO : Imprime informações de erros e exceções, indicando que embora ocorra um evento de erro, ele ainda não afeta a operação contínua do sistema.
- FATAL : Indica que cada evento de erro grave fará com que o aplicativo seja encerrado. Erros graves, este nível pode interromper diretamente o programa.
- OFF : O nível mais alto, usado para desligar todos os registros.
Pode-se observar que nossa solicitação não configura um cookie,
que é a causa raiz da falha da solicitação fegin, portanto configuramos o fegin para enviar a solicitação com um cookie no software.
Fingir problema de cookie perdido
Como o fegin enviará uma nova solicitação toda vez que solicitar, ao invés de trazer o cookie da nossa solicitação anterior, teremos que configurá-lo manualmente neste momento. Continue adicionando a configuração onde a depuração foi configurada antes, injete um interceptador no contêiner Spring e defina um cookie antes da solicitação Feign
@Configuration
public class FeignConfig {
@Bean
public feign.Logger logger() {
return new Slf4jLogger();
}
@Bean
public Logger.Level level() {
return Logger.Level.FULL;
}
@Bean("requestInterceptor")
public RequestInterceptor requestInterceptor() {
RequestInterceptor requestInterceptor = new RequestInterceptor() {
@Override
public void apply(RequestTemplate template) {
//1、使用RequestContextHolder拿到刚进来的请求数据
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (requestAttributes != null) {
//老请求
HttpServletRequest request = requestAttributes.getRequest();
//2、同步请求头的数据(主要是cookie)
//把老请求的cookie值放到新请求上
String cookie = request.getHeader("Cookie");
template.header("Cookie", cookie);
}
}
};
return requestInterceptor;
}
}
Verifique o log e descubra que a solicitação trouxe um cookie com sucesso.
Logicamente falando, as duas solicitações deveriam ser um cookie e uma sessão, mas aqui descobrimos que as duas sessões são inconsistentes.
Provavelmente o tempo de login expirou. Basta fazer login novamente e tudo ficará bem.
A mensagem foi retornada com sucesso.
Por que os microsserviços anteriores não tiveram problemas?
Eu ajustei o servidor de autenticação e terceiros e o servidor de autenticação e membro antes, mas nenhum problema semelhante ocorreu.
A razão para o primeiro é que terceiros não possuem um interceptador de login, portanto, quando o servidor de autenticação chama terceiros, ele não retornará conteúdo de texto/html, para que possa ser analisado normalmente. Como não existe interceptador de login, a presença ou ausência de cookies não afeta as chamadas remotas.
A razão para este último é que embora o membro tenha um interceptador de login, porque a interface solicitada pelo servidor de autenticação é liberada (veja o código acima para detalhes), o valor de retorno de text/html não será retornado, então pode ser analisado normalmente. Ao mesmo tempo, como a interface não requer cookies de autenticação de login, isso não afetará a perda do cookie do cabeçalho da solicitação fegin.