Spring Cloud版——电影售票系统<三>使用Feign实现声明式REST调用

原创
2017/09/11 23:20
阅读数 1.3K
AI总结

一、Feign简介

        Feign是Netflix开发的声明式、模块化的HTTP客户端,其灵感来自Retrofit, JAXRS-2.0以及WebSocket。Feign可帮助我们更好更快的便捷、优雅地调用HTTP API。

        在Spring Cloud中,使用Feign非常简单——创建一个接口,并在接口上添加一些注释,代码就OK了。Feign 支持多种注释,例如Feign自带的注解或者JAX-RS注解等。

        Spring Cloud对Feign进行了增强,使Feign支持了Spring MVC注解,并整合了Ribbon和 Eureka,从而让Feign 的使用更加方便。

 

二、为服务消费者整合Feign

        之前的电影微服务是使用RestTemplate(负载均衡通过整合Ribbon实现)调用RESTful API的。现在进一步完善优化项目使用Feign,实现声明式的RESTful API调用。

         添加Feign的依赖:

    

        创建一个Feign接口,并添加@FeignClient注解:

        Tips: @FeignClient注解中的"movieticketing-provider-user"是一个任意的客户端名称,用于创建Ribbon负载均衡器。由于使用了Eureka ,所以Ribbon会把"movieticketing-provider-user"解析成Eureka Server服务注册表中的服务。当然也可以,使用service.ribbon.listOfServer属性配置;还可以使用,url属性指定请求的URL (URL可以是完整的URL或者主机名),例如@FeignClient(name = "movieticketing-provider-user", url = "http://localhost:8000/")

        修改Controller,让其调用Feign接口:

        修改启动类,为其添加@EnableFeignClients注解:

 

三、自定义Feign配置

        在Spring Cloud中,Feign的默认配置类是FeignClientsConfiguration, 该类定义了Feign默认使用的编码器、解码器、所使用的契约等。

        Spring Cloud 允许通过注解@FeignClient的configuration属性自定义Feign的配置,自定义配置的优先级比FeignClientsConfiguration更高。在Spring Cloud中,Feign默认使用的契约是SpringMvcContract,因此它可以使用Spring MVC的注解。

 

四、手动创建Feign
 

        用户微服务的接口需要登录后才能调用,并且对于相同的API,不同角色的用户有不同的行为;让电影微服务中的同一个Feign接口,使用不同的账号登录,并调用用户微服务的接口。

        首先,为项目添加以下依赖:

        然后,创建Spring Security的配置类:

package com.binggo.springcloud.micro.security;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.Collection;

/**
 * Spring Security 的配置类
 * @author binggo
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    // 所有的请求,都需要经过HTTP basic认证
    http.authorizeRequests().anyRequest().authenticated().and().httpBasic();
  }

  @Bean
  public PasswordEncoder passwordEncoder() {
    // 明文编码器。这是一个不做任何操作的密码编码器,是Spring提供给我们做明文测试的。A password encoder that does nothing. Useful for testing where working with plain text
    return NoOpPasswordEncoder.getInstance();
  }

  @Autowired
  private CustomUserDetailsService userDetailsService;

  @Override
  protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(this.userDetailsService).passwordEncoder(this.passwordEncoder());
  }

  @Component
  class CustomUserDetailsService implements UserDetailsService {
    /**
     * 模拟两个账户:
     * ① 账号是user,密码是user,角色是user-role
     * ② 账号是admin,密码是admin,角色是admin-role
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
      if ("user".equals(username)) {
        return new SecurityUser("user", "user", "user-role");
      } else if ("admin".equals(username)) {
        return new SecurityUser("admin", "admin", "admin-role");
      } else {
        return null;
      }
    }
  }

  class SecurityUser implements UserDetails {
    private static final long serialVersionUID = 1L;

    public SecurityUser(String username, String password, String role) {
      super();
      this.username = username;
      this.password = password;
      this.role = role;
    }

    public SecurityUser() {
    }

    private Long id;
    private String username;
    private String password;
    private String role;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
      Collection<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
      SimpleGrantedAuthority authority = new SimpleGrantedAuthority(this.role);
      authorities.add(authority);
      return authorities;
    }

    @Override
    public boolean isAccountNonExpired() {
      return true;
    }

    @Override
    public boolean isAccountNonLocked() {
      return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
      return true;
    }

    @Override
    public boolean isEnabled() {
      return true;
    }

    @Override
    public String getPassword() {
      return this.password;
    }

    @Override
    public String getUsername() {
      return this.username;
    }

    public Long getId() {
      return this.id;
    }

    public void setId(Long id) {
      this.id = id;
    }

    public void setUsername(String username) {
      this.username = username;
    }

    public void setPassword(String password) {
      this.password = password;
    }

    public String getRole() {
      return this.role;
    }

    public void setRole(String role) {
      this.role = role;
    }
  }
}

        其次,修改Controller,测试打印当前登录的用户信息:

package com.binggo.springcloud.micro.controller;

import com.binggo.springcloud.micro.entity.User;
import com.binggo.springcloud.micro.repository.UserRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import java.util.Collection;

/**
 * UserController
 * @author binggo
 */
@RestController
public class UserController {
    @Autowired
    private UserRepository userRepository;
    
    private static final Logger LOGGER = LoggerFactory.getLogger(UserController.class);

  /*  @GetMapping("/{id}")
    public User findById(@PathVariable Long id){
        User findOne = this.userRepository.findOne(id);
        return findOne;
    }*/

    /**
     * 打印当前登录的用户信息
     * @author libingbin2015@aliyun.com
     */
    @GetMapping("/{id}")
    public User findById(@PathVariable Long id) {
        Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        if (principal instanceof UserDetails) {
            UserDetails user = (UserDetails) principal;
            Collection<? extends GrantedAuthority> collection = user.getAuthorities();
            for (GrantedAuthority c : collection) {
                // 打印当前登录用户的信息
                UserController.LOGGER.info("当前用户是{},角色是{}", user.getUsername(), c.getAuthority());
            }
        } else {
            // do other things
        }
        User findOne = this.userRepository.findOne(id);
        return findOne;
    }
    
}

        调用后,弹出登录对话框,输入账号密码,不同的账号不同的调用。

 

五、Feign支持继承

        使用继承,可将一些公共操作分组到一些父接口中,从而简化Feign的开发。尽管Feign的继承可帮助我们进一步简化Feign的开发,但Spring Cloud官方也指出——通常情况下,不建议在服务器端与客户端之间共享接口,因为这种方式会造成服务器端和客户端代码的紧耦合。并且,Feign本身并不使用Spring MVC的工作机制(方法参数映射不被继承)。但我个人认为,放弃开发的方便性或者接受代码的紧耦合,应该在具体问题下权衡利弊,取其利。

"All problems in computer science can be solved by another level of indirection.”
– David J. Wheeler
“计算机世界就是 trade-off 的艺术”

 

六、Feign对日志的处理

        Feign对日志的处理非常灵活,可为每个Feign客户端指定日志记录策略,每个Feign客户端都会创建一个logger。默认下,logger的名称是Feign接口的完整类名。但是,Feign的日志打印只会对DEBUG级别做出响应。我们可为每个Feign客户端配置各自的Logger.Level对象,Logger.Level的值有以下选择:

NONE:不记录任何日志(默认值)
BASIC:仅记录请求方法、URL、响应状态代码以及执行时间
HEADERS:记录BASIC级别基础上,记录请求和响应的header
FULL:记录请求和响应的header、body和元数据

        编写Feign配置类:

        修改Feign接口,指定配置类:

        在application.yml中添加以下内容:

七、也可以使用Feign构造多参数请求
 

        假设需请求的URL包含多个参数,例如http://localhost/get?id=1&username=张三 ,该如何使用Feign构造呢?我们知道,Spring Cloud为Feign添加了Spring MVC的注解支持,那么我们不妨按照Spring MVC的写法尝试一下:


@FeignClient("microservice-provider-user")
public interface UserFeignClient {
  @RequestMapping(value = "/get", method = RequestMethod.GET)
  public User get0(User user);
}

        然而,这种写法并不正确,控制台会输出类似如下的异常。

feign.FeignException: status 405 reading UserFeignClient#get0(User); content:
{"timestamp":1482688142940,"status":405,"error":"Method Not Allowed","exception":"org.springframework.web.HttpRequestMethodNotSupportedException","message":"Request method 'POST' not supported","path":"/get"}


        由异常可知,尽管我们指定了GET方法,Feign依然会使用POST方法发送请求。于是导致了异常。正确写法如下

        方法一[推荐]

@FeignClient("microservice-provider-user")
public interface UserFeignClient {
  @GetMapping("/get")
  public User get0(@SpringQueryMap User user);
}

        方法二[推荐]
 

@FeignClient(name = "microservice-provider-user")
public interface UserFeignClient {
  @RequestMapping(value = "/get", method = RequestMethod.GET)
  public User get1(@RequestParam("id") Long id, @RequestParam("username") String username);
}

        这是最为直观的方式,URL有几个参数,Feign接口中的方法就有几个参数。使用@RequestParam注解指定请求的参数是什么。
 

        方法三[不推荐]

        多参数的URL也可使用Map来构建。当目标URL参数非常多的时候,可使用这种方式简化Feign接口的编写。

@FeignClient(name = "microservice-provider-user")
public interface UserFeignClient {
  @RequestMapping(value = "/get", method = RequestMethod.GET)
  public User get2(@RequestParam Map<String, Object> map);
}


        在调用时,可使用类似以下的代码。
 

public User get(String username, String password) {
  HashMap<String, Object> map = Maps.newHashMap();
  map.put("id", "1");
  map.put("username", "张三");
  return this.userFeignClient.get2(map);
}


        注意:这种方式不建议使用。主要是因为可读性不好,而且如果参数为空的时候会有一些问题,例如map.put("username", null); 会导致microservice-provider-user 服务接收到的username是"" ,而不是null。


        POST请求包含多个参数

        下面来讨论如何使用Feign构造包含多个参数的POST请求。假设服务提供者的Controller是这样编写的:
 

@RestController
public class UserController {
  @PostMapping("/post")
  public User post(@RequestBody User user) {
    ...
  }
}


        我们要如何使用Feign去请求呢?答案非常简单,示例:

@FeignClient(name = "microservice-provider-user")
public interface UserFeignClient {
  @RequestMapping(value = "/post", method = RequestMethod.POST)
  public User post(@RequestBody User user);
}

 

--------------------------------------

版权声明:本文为【PythonJsGo】博主的原创文章,转载请附上原文出处链接及本声明。

博主主页:https://my.oschina.net/u/3375733

本篇文章同步在个人公众号:

 

展开阅读全文
加载中
点击引领话题📣 发布并加入讨论🔥
0 评论
1 收藏
0
分享
AI总结
返回顶部
顶部