Spring Cloud版——电影售票系统<三>使用Feign实现声明式REST调用
Spring Cloud版——电影售票系统<三>使用Feign实现声明式REST调用
libingbin 发表于1个月前
Spring Cloud版——电影售票系统<三>使用Feign实现声明式REST调用
  • 发表于 1个月前
  • 阅读 8
  • 收藏 0
  • 点赞 0
  • 评论 0

腾讯云实验室 1小时搭建人工智能应用,让技术更容易入门 免费体验 >>>   

    GitHub地址:https://github.com/leebingbin/SpringCloud.MovieTicketing

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

一、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

    为项目添加以下依赖:

    创建Spring Security的配置类:

package com.bingbinlee.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 libingbin2015@aliyun.com
 */
@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.bingbinlee.springcloud.micro.controller;

import com.bingbinlee.springcloud.micro.entity.User;
import com.bingbinlee.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 libingbin2015@aliyun.com
 */
@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的工作机制(方法参数映射不被继承)。但我个人认为,放弃开发的方便性或者接受代码的紧耦合,应该在具体问题下权衡利弊,取其利。

六、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构造多参数请求

共有 人打赏支持
粉丝 2
博文 56
码字总数 66998
×
libingbin
如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
* 金额(元)
¥1 ¥5 ¥10 ¥20 其他金额
打赏人
留言
* 支付类型
微信扫码支付
打赏金额:
已支付成功
打赏金额: