分类目录编程

异常处理 Exception

spring mvc提供三种异常处理器:
DefaultHandlerExceptionResolver, ResponseStatusExceptionResolver, ExceptionHandlerExceptionResolver

为了统一异常处理,我们可以自定义一个HandlerExceptionResolver,所有的异常均由自定义的异常处理, 由此我们可以重写DispatcherServlet类的
processHandlerException()方法:

  1. @ExceptionHandler
    使用@ExceptionHandler注解作用在方法上面,参数是具体的异常类型。一旦系统抛出这种类型的异常时,会引导到该方法来处理。但是它的缺陷很明显,处理异常的方法和出错的方法(或者异常最终抛出来的地方)必须在同一个controller,不能全局控制。

  2. @ControllerAdvice + @ExceptionHandler
    使用@ControllerAdvice 和@ExceptionHandler 可以全局控制异常,使业务逻辑和异常处理分隔开。

自定异常
@Data
public class UserNotExitsException extends RuntimeException{
    private Object id;
    public UserNotExitsException(Object id){
        super("用户不存在");
        this.id = id;
    }
}
全局控制异常
@ControllerAdvice
@Slf4j
public class ControllerExceptionHandler {

    /**
     * 用户不存在
     * 待处理异常的类 UserNotExitsException
     * @return 将返回的信息转为JSON,同时返回HTTP状态码500
     */
    @ExceptionHandler(UserNotExitsException.class)
    @ResponseBody
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public JsonResult handleUserNotExitException(UserNotExitsException ex){
        log.info("handleUserNotExitException:"+String.valueOf(ex.getId()));
        return JsonResult.error(ex.getMessage(),ex.getId());
    }
}

优先级

既然在SpringMVC中有两种处理异常的方式,那么就存在一个优先级的问题:

当发生异常的时候,SpringMVC会如下处理:

(1)SpringMVC会先从配置文件找异常解析器HandlerExceptionResolver

(2)如果找到了异常异常解析器,那么接下来就会判断该异常解析器能否处理当前发生的异常

(3)如果可以处理的话,那么就进行处理,然后给前台返回对应的异常视图

(4)如果没有找到对应的异常解析器或者是找到的异常解析器不能处理当前的异常的时候,就看当前的Controller中有没有提供对应的异常处理器,如果提供了就由Controller自己进行处理并返回对应的视图

(5)如果配置文件里面没有定义对应的异常解析器,而当前Controller中也没有定义的话,就看有没有全局ControllerAdvice提供的全局异常处理器,如果没有那么该异常就会被抛出来。

链路监控 Spring Cloud Sleuth & Zipkin

链路监控 Spring Cloud Sleuth

        <!--<dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-sleuth</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-sleuth-zipkin</artifactId>
        </dependency>-->
        <!--已经包括sleuth和zipkin-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-zipkin</artifactId>
        </dependency>

Zipkin

docker run -itd --name zipkin -p 9411:9411 openzipkin/zipkin

spring:
  zipkin:
    base-url: http://192.168.88.108:9411
 sleuth:
    sampler:
      probability: 1

Spring Cloud Stream 分组

服务 介绍
stream-group-sender 消息发送者服务
stream-group-receiverA 消息接收者服务
stream-group-receiverB 消息接收者服务

创建stream-group-sender 服务

spring.application.name=stream-sender
//对应 MQ 是 exchange outputProduct自定义的信息
spring.cloud.stream.bindings.outputProduct.destination=exchangeProduct
1. 定义发送接口

public interface ISendeService {
    String OUTPUT="outputProduct";
    /**
     * 指定输出的交换器名称
     * @return
     */
    @Output(OUTPUT)
    MessageChannel send();
}
  1. 在启动类增加注解
    // 绑定我们刚刚创建的发送消息的接口类型
    @EnableBinding(value={ISendeService.class})
  2. 测试发送
@RunWith(SpringRunner.class)
@SpringBootTest(classes=StreamSenderStart.class)
public class StreamTest {
    @Autowired
    private ISendeService sendService;

    @Test
    public void testStream(){
        Product p = new Product(666, "stream test ...");
        // 将需要发送的消息封装为Message对象
        Message message = MessageBuilder
                                .withPayload(p)
                                .build();
        sendService.send().send(message );
    }
}

创建stream-group-receiverA服务

spring.application.name=stream-group-receiverA
// 对应 MQ 是 exchange 和消息发送者的 交换器是同一个
spring.cloud.stream.bindings.inputProduct.destination=exchangeProduct
// 具体分组 对应 MQ 是 队列名称 并且持久化队列 inputProduct 自定义
spring.cloud.stream.bindings.inputProduct.group=groupProduct
1. 定义接收口

public interface IReceiverService {
    String INPUT = "inputProduct";
    /**
     * 指定接收的交换器名称
     * @return
     */
    @Input(INPUT)
    SubscribableChannel receiver();
}
  1. 消息的具体处理
@Service
@EnableBinding(IReceiverService.class)
public class ReceiverService {
    @StreamListener(IReceiverService.INPUT)
    public void onReceiver(Product p){
        System.out.println("消费者A:"+p);
    }
}
  1. 在启动类添加注解
    @EnableBinding(value={IReceiverService.class})

创建stream-group-receiverB服务

 此服务和stream-group-receiverA一样,复制一份只需修改配制中的服务名称,端口,group设置不一样
在stream-group-receiverA和stream-group-receiverB服务的group不一致的情况下都收到了消息
改为同组的情况下只有其中一个受到消息。避免了消息重复消费

RabbitMq使用

用于异步处理,日志处理,流量削峰,应用解耦

引用依赖

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-bus-amqp</artifactId>
        </dependency>

参数配制

默认配制
spring:
 rabbitmq:
   host: localhost
   port: 5672
   username: guest
   password: guest

发送消息

public class MqSend {
    @Autowired
    private AmqpTemplate amqpTemplate;

     /**
     * 发送Mq测试消息
     */
    public void send(){
        amqpTemplate.convertAndSend("myQueue","now "+new Date());
    }

    /**
     * 发送数据供应商分组Mq测试消息
     */
    public void sendOrder(){
        amqpTemplate.convertAndSend("myQueue","computer","now "+new Date());
    }

}

接收消息

@Slf4j
@Component
public class MqReceiver {

    //1. 接收手动创建的消息,需先手动创建myQueue队列名
    /*@RabbitListener(queues = "myQueue")
    public void processtest(String message){
        log.info("MQReceiver: {}",message);
    }*/

    //2. 自动创建队列
    // @RabbitListener(queuesToDeclare = @Queue("myQueue"))

    //3. 自动创建, Exchange和Queue绑定
    @RabbitListener(bindings = @QueueBinding(
            value = @Queue("myQueue"),
            exchange = @Exchange("myExchange")
    ))
    public void process(String message) {
        log.info("MqReceiver: {}", message);
    }
     /**
     * 数码供应商服务 接收分组消息
     * @param message
     */
    @RabbitListener(bindings = @QueueBinding(
            exchange = @Exchange("myOrder"),
            key = "computer",
            value = @Queue("computerOrder")
    ))
    public void processComputer(String message) {
        log.info("computer MqReceiver: {}", message);
    }

    /**
     * 水果供应商服务 接收分组消息
     * @param message
     */
    @RabbitListener(bindings = @QueueBinding(
            exchange = @Exchange("myOrder"),
            key = "fruit",
            value = @Queue("fruitOrder")
    ))
    public void processFruit(String message) {
        log.info("fruit MqReceiver: {}", message);
    }
}

Feign/RestTemplate

  1. 增加依赖
         <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
  1. 在启动主类增加注解
@EnableFeignClients
  1. 定义feign调用商品微服务的接口
访问PRODUCT服务下的接口
@FeignClient(name = "PRODUCT")
public interface ProductClient {

    /**
     * 从商品微服务中获取一个测试信息
     * @return
     */
    @GetMapping("/msg")
    String productMsg();

    /**
     * 根据一组商品ID获取商品列表
     * @param productIdList
     * @return
     */
    @PostMapping("/listForOrder")
    List<ProductInfoOutput> listForOrder(@RequestBody List<String> productIdList);

    /**
     * 扣库存
     * @param decreaseStockInputList
     */
    @PostMapping("/decreaseStock")
    void decreaseStock(@RequestBody List<DecreaseStockInput> decreaseStockInputList);
}

  1. 调用feign接口
@Api(tags="RestTemplate/feign调用微服务API")
@RestController
@Slf4j
public class ClientController {
    /*第二种方式*/
   /* @Autowired
    private LoadBalancerClient loadBalancerClient;*/

    /* 第三种方式
     @Autowired
    private RestTemplate restTemplate;*/

    /*第四种方式feign*/
    @Autowired
    private ProductClient productClient;

    @ApiOperation("获取一个测试信息")
    @GetMapping("/msg")
    public String getProductMsg(){
        //第一种方式 直接使用RestTemplate,URL固定
        //RestTemplate restTemplate = new RestTemplate();
        //String response = restTemplate.getForObject("http://localhost:8080/productserver/msg",String.class);

        //第二种方式 直接使用RestTemplate,URL利用LoadBalancerClient获取
        /*RestTemplate restTemplate = new RestTemplate();
        ServiceInstance serviceInstance = loadBalancerClient.choose("PRODUCT");
        String url = String.format("http://%s:%s/msg",serviceInstance.getHost(),serviceInstance.getPort());
        String response = restTemplate.getForObject(url,String.class);*/

        //第三种,将 RestTemplate 作为一个Bean 配制到项目,使用注解@LoadBalanced,在restTemplate直接使用应用名字
        //String response = restTemplate.getForObject("http://PRODUCT/msg",String.class);
        //4.feign
        String response = productClient.productMsg();
        log.info("response={}",response);
        return response;
    }
    }
    第三种方式使用注解@LoadBalanced的配制文件
    @Component
    public class RestTemplateConfig {
     @Bean
    @LoadBalanced
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }
    or
    @Bean
    @LoadBalanced
    public RestTemplate restTemplate(){
        //return new RestTemplate();
        return new RestTemplateBuilder().basicAuthentication("oauth", "123").build();
    }
    }

HttpHeaders header = new HttpHeaders();
header.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
MultiValueMap<String, String> map= new LinkedMultiValueMap<>();
map.add(“grant_type”, “password”);
map.add(“client_id”, “oauth”);
HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(map, header);
MyToken myToken = restTemplate.postForObject(“http://192.168.88.108:30002/oauth/token”,request,MyToken.class);

    // build http headers
    HttpHeaders headers = new HttpHeaders();
    headers.add("Authorization","bearer "+myToken.getAccess_token());
    ResponseEntity<ClientUser[]> responseEntity = restTemplate.exchange(RBAC_SERVER+POST_ALL_USER, HttpMethod.GET,new HttpEntity<String>(headers),ClientUser[].class);

Ribbon 客户端负载均衡

更改负载均衡规则

PRODUCT:
  ribbon:
    NFLoadBalancerRuleClassName: com.loadbalancer.RandomRule

PRODUCT为应用名,此处改为随机方式,注意写完整的Class路径,不改默认为轮询,一般也够用了,其它参考官方说明文档

Hystrix熔断

具有服务降级(HystrixCommand注解指定/fallbackMethod回退函数实现降级逻辑),服务熔断,依赖隔离,监控的作用,防雪崩的利器

        <!--添加Hystrix依赖-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
           <!-- <version>2.0.2.RELEASE</version>-->
        </dependency>
        <!--添加Hystrix dashboard依赖-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
            <!-- <version>2.0.2.RELEASE</version>-->
        </dependency>
        <!--如果有了不用再引入-->
        <!--<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>-->
在主函数上添加注解
@EnableCircuitBreaker //启动Hystrix
@EnableHystrixDashboard

@HystrixCommand
如果我们使用的是@HystrixCommand注解,那么可以在注解中直接指定超时时间,如下:

@HystrixCommand(fallbackMethod="fallback",
    commandProperties = {
         @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "1000" )
    }
)

当然也可以指定commandKey,然后在配置文件中配置超时时间,如下:

@HystrixCommand(fallbackMethod="fallback",commandKey="userGetKey")
配置文件给commandKey配置超时时间:
hystrix.command.userGetKey.execution.isolation.thread.timeoutInMilliseconds = 13000

全局配置
如果只是想全局的配置,可以配置默认的超时时间:

hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=3000

接口级别配置
假如我们的Feign Client定义如下:

@FeignClient(value = "user-service", fallbackFactory = UserRemoteClientFallbackFactory.class)
public interface UserRemoteClient {

    @GetMapping("/user/get")
    public ResponseData<UserDto> getUser(@RequestParam("id") Long id);

}

那么配置如下:

hystrix.command.UserRemoteClient#getUser(Long).execution.isolation.thread.timeoutInMilliseconds = 300

为什么要配置成上面的方式呢?

其实就是对commandKey进行配置,只要我们知道commandKey的生成规则就可以对接口级别进行配置,接口级别的规则是 Client名称#方法名(参数类型)

源码在feign.hystrix.SetterFactory.Default中:

String commandKey = Feign.configKey(target.type(), method);
服务级别配置
1.在Zuul中针对服务级别的话,直接配置service-id,如下:

hystrix.command.service-id.execution.isolation.thread.timeoutInMilliseconds=3000
Zuul中之所以要配置service-id原因是commandKey就是用的service-id, 通过源码分析可以得到结论。

首先进入的是RibbonRoutingFilter中的run方法,然后我们看核心的forward方法:

ClientHttpResponse response = forward(commandContext);
在forward中有下面的代码:

RibbonCommand command = this.ribbonCommandFactory.create(context);
通过create可以定位到具体的实现,这边就看你用的什么Http客户端,默认有三种实现,默认定位到org.springframework.cloud.netflix.zuul.filters.route.apache.HttpClientRibbonCommandFactory.create(RibbonCommandContext)方法。
所以service-id就是commandKey。

2.在Feign中针对服务级别的话,需要对commandKey进行定制,可以用service-id, 也可以用Feign Client Name,如下:

@Bean
@Scope("prototype")
@ConditionalOnMissingBean
@ConditionalOnProperty(name = "feign.hystrix.enabled")
public Feign.Builder feignHystrixBuilder() {
    return HystrixFeign.builder().setterFactory(new SetterFactory() {

        @Override
        public Setter create(Target<?> target, Method method) {
            String groupKey = target.name();
            String commandKey = Feign.configKey(target.type(), method);
            return HystrixCommand.Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey(groupKey))
                        //.andCommandKey(HystrixCommandKey.Factory.asKey(commandKey))
                        //.andCommandKey(HystrixCommandKey.Factory.asKey(groupKey))
                        .andCommandKey(HystrixCommandKey.Factory.asKey(target.type().getSimpleName()));
            }
    });
}
.andCommandKey(HystrixCommandKey.Factory.asKey(commandKey))
默认的接口方式

.andCommandKey(HystrixCommandKey.Factory.asKey(groupKey))
service-id方式

.andCommandKey(HystrixCommandKey.Factory.asKey(target.type().getSimpleName()));
Feign Client Name方式

配置的话根据不同的配置填写不通的commandKey就可以了:

hystrix.command.Feign Client Name.execution.isolation.thread.timeoutInMilliseconds=3000

swagger2

1、pom.xml 添加 Maven 依赖

<dependencies>
    <!-- https://mvnrepository.com/artifact/io.springfox/springfox-swagger-ui -->
    <dependency>
        <groupId>io.springfox</groupId>
        <artifactId>springfox-swagger-ui</artifactId>
        <version>2.9.2</version>
    </dependency>
​
 <!-- https://mvnrepository.com/artifact/io.springfox/springfox-swagger2 -->
    <dependency>
       <groupId>io.springfox</groupId>
       <artifactId>springfox-swagger2</artifactId>
       <version>2.9.2</version>
    </dependency>
</dependencies>

2、创建 Swagger2Configuration.java

@Configuration
@EnableSwagger2
public class Swagger2Configuration {
    //api接口包扫描路径
    public static final String SWAGGER_SCAN_BASE_PACKAGE = "com.hzdtech.order.server";
    public static final String VERSION = "1.0.0";

    @Bean
    public Docket createRestApi() {
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .select()
                .apis(RequestHandlerSelectors.basePackage(SWAGGER_SCAN_BASE_PACKAGE))
                .paths(PathSelectors.any())// 可以根据url路径设置哪些请求加入文档,忽略哪些请求
                .build();
    }

    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("订单微服务") //设置文档的标题
                .description("单词计数服务 API 接口文档") // 设置文档的描述
                .version(VERSION) // 设置文档的版本信息-> 1.0.0 Version information
                .termsOfServiceUrl("http://shanpai.video") // 设置文档的License信息->1.3 License information
                .build();

    }
}

3、API 接口编写

@RestController
@Api(tags="接口所在的类")
@RequestMapping ("/my")
public class MyController {

    @RequestMapping(value="/list", method=RequestMethod.POST)
    @ApiOperation(value = "接口名", notes = "接口描述", httpMethod = "POST")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "length",value = "参数1", required = true, paramType = "path"),
            @ApiImplicitParam(name = "size",value = "参数2", required = true, paramType = "query"),
            @ApiImplicitParam(name = "page",value = "参数3", required = true, paramType = "header"),
            @ApiImplicitParam(name = "total",value = "参数4", required = true, paramType = "form"),
            @ApiImplicitParam(name = "start",value = "参数5",dataType = "string", paramType = "body")
    })
    public String register(){
        return "has permission";
    }
}
@Api:修饰整个类,描述Controller的作用
@ApiOperation:描述一个类的一个方法,或者说一个接口
@ApiParam:单个参数描述
@ApiModel:用对象来接收参数
@ApiProperty:用对象接收参数时,描述对象的一个字段
@ApiResponse:HTTP响应其中1个描述
@ApiResponses:HTTP响应整体描述
@ApiIgnore:使用该注解忽略这个API
@ApiError :发生错误返回的信息
@ApiImplicitParam:描述一个请求参数,可以配置参数的中文含义,还可以给参数设置默认值
@ApiImplicitParams:描述由多个 @ApiImplicitParam 注解的参数组成的请求参数列表

4、启动 SpringBoot 应用

SpringBoot 启动成功后,访问 http://localhost:8080/swagger-ui.html

###5 5、在 Security 中的配置

Spring Boot 项目中如果集成了 Spring Security,在不做额外配置的情况下,Swagger2 文档会被拦截。解决方法是在 Security 的配置类中重写 configure 方法添加白名单即可:

@Override
public void configure ( WebSecurity web) throws Exception {
    web.ignoring()
      .antMatchers("/swagger-ui.html")
      .antMatchers("/v2/**")
      .antMatchers("/swagger-resources/**");
}