GVKun编程网logo

手写SpringBoot自动配置及自定义注解搭配Aop,实现升级版@Value()功能(springboot自己写注解)

23

本文将为您提供关于手写SpringBoot自动配置及自定义注解搭配Aop,实现升级版@Value()功能的详细介绍,我们还将为您解释springboot自己写注解的相关知识,同时,我们还将为您提供关于

本文将为您提供关于手写SpringBoot自动配置及自定义注解搭配Aop,实现升级版@Value()功能的详细介绍,我们还将为您解释springboot自己写注解的相关知识,同时,我们还将为您提供关于java/springboot 自定义注解实现 AOP、java基础复习-自定义注解3(自定义注解在SpringBoot中的使用)、spring boot aop 打印出入参配置(自定义注解)、Spring Boot 自动配置的原理、核心注解以及利用自动配置实现了自定义 Starter 组件的实用信息。

本文目录一览:

手写SpringBoot自动配置及自定义注解搭配Aop,实现升级版@Value()功能(springboot自己写注解)

手写SpringBoot自动配置及自定义注解搭配Aop,实现升级版@Value()功能(springboot自己写注解)

背景

项目中为了统一管理项目的配置,比如接口地址,操作类别等信息,需要一个统一的配置管理中心,类似nacos。
我根据项目的需求写了一套分布式配置中心,测试无误后,改为单体应用并耦合到项目中。项目中使用配置文件多是取配置文件(applicatoion.yml)的值,使用@Value获取,为了秉持非侵入性的原则,我决定写一套自定义注解,以实现最少的代码量实现业务需求。

思路

需要实现类似springboot @Value注解获取配置文件对应key的值的功能。但区别在于 我是从自己写的自动配置中获取,原理就是数据库中查询所有的配置信息,并放入一个对象applicationConfigContext,同时创建一个bean交给spring托管,同时写了个aop,为被注解的属性赋入applicationConfigContext的对应的值。
换句话说,自定义的这个注解为类赋值的时间线大概是

 spring bean初始化 —->  第三方插件初始化 --> 我写的自动配置初始化   ---- 用户调用某个方法,触发aop机制,我通过反射动态改变了触发aop的对象的bean的属性,将值赋值给他。

难点

本项目的难点在于如何修改对象的值。看似简单,其实里面的文章很多。

自动配置代码

配置映射数据库pojo

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.Date;

/**
 * @Describtion config bean
 * @Author yonyong
 * @Date 2020/7/13 15:43
 * @Version 1.0.0
 **/
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder(toBuilder = true)
public class TblConfig {
    private Integer id;

    /**
     * 配置名称
     */
    private String keyName;

    /**
     * 默认配置值
     */
    private String keyValue;

    /**
     * 分类
     */
    private String keyGroup;

    /**
     * 备注
     */
    private String description;

    /**
     * 创建时间
     */
    private Date insertTime;

    /**
     * 更新时间
     */
    private Date updateTime;

    /**
     * 创建人
     */
    private String creator;

    private Integer start;

    private Integer rows;

    /**
     * 是否是系统自带
     */
    private String type;

    /**
     * 修改人
     */
    private String modifier;
}

创建用于防止配置信息的对象容器

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.List;
import java.util.stream.Collectors;

/**
 * @Describtion config container
 * @Author yonyong
 * @Date 2020/7/13 15:40
 * @Version 1.0.0
 **/
@Data
@Builder(toBuilder = true)
@AllArgsConstructor
@NoArgsConstructor
public class ConfigContext {

    /**
     * config key-val map
     */
    private List<TblConfig> vals;

    /**
     * env type
     */
    private String group;

    /**
     * get config
     * @param key
     * @return
     */
    public String getValue(String key){
        final List<TblConfig> collect = vals.stream()
                .filter(tblConfig -> tblConfig.getKeyName().equals(key))
                .collect(Collectors.toList());
        if (null == collect || collect.size() == 0)
            return null;
        return collect.get(0).getKeyValue();
    }
}

创建配置,查询出数据库里配置并创建一个容器bean

import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;

import javax.annotation.Resource;
import java.util.List;

/**
 * @Describtion manual auto inject bean
 * @Author yonyong
 * @Date 2020/7/13 15:55
 * @Version 1.0.0
 **/
@Configuration
@ConditionalOnClass(ConfigContext.class)
public class ConfigContextAutoConfig {

    @Value("${config.center.group:DEFAULT_ENV}")
    private String group;

    @Resource
    private TblConfigcenterMapper tblConfigcenterMapper;

    @Bean(name = "applicationConfigContext")
    @Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)
    @ConditionalOnMissingBean(ConfigContext.class)
    public ConfigContext myConfigContext() {
        ConfigContext configContext = ConfigContext.builder().build();
        //set group
        if (StringUtils.isNotBlank(group))
            group = "DEFAULT_ENV";
        //set vals
        TblConfig tblConfig = TblConfig.builder().keyGroup(group).build();
        final List<TblConfig> tblConfigs = tblConfigcenterMapper.selectByExample(tblConfig);
        configContext = configContext.toBuilder()
                .vals(tblConfigs)
                .group(group)
                .build();
        return configContext;
    }
}

AOP相关代码

创建自定义注解

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @Author yonyong
 * @Description //配置
 * @Date 2020/7/17 11:20
 * @Param 
 * @return 
 **/
@Target({ElementType.FIELD,ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyConfig {
    /**
     * 如果此value为空,修改值为获取当前group,不为空正常获取配置文件中指定key的val
     * @return
     */
    String value() default "";
    Class<?> clazz() default MyConfig.class;
}

创建aop业务功能

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Date;

/**
 * @Describtion config service aop
 * @Author yonyong
 * @Date 2020/7/17 11:21
 * @Version 1.0.0
 **/
@Aspect
@Component
@Slf4j
public class SystemConfigAop {

    @Autowired
    ConfigContext applicationConfigContext;

    @Autowired
    MySpringContext mySpringContext;

    @Pointcut("@annotation(com.ai.api.config.configcenter.aop.MyConfig)")
    public void pointcut(){}

    @Before("pointcut()")
    public void before(JoinPoint joinPoint){
        final MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        MyConfig myConfig = method.getAnnotation(MyConfig.class);
        Class<?> clazz = myConfig.clazz();
        final Field[] declaredFields = clazz.getDeclaredFields();
        Object bean = mySpringContext.getBean(clazz);
        for (Field declaredField : declaredFields) {
            final MyConfig annotation = declaredField.getAnnotation(MyConfig.class);
            if (null != annotation && StringUtils.isNotBlank(annotation.value())){
                log.info(annotation.value());
                String val = getVal(annotation.value());
                try {
//                    setFieldData(declaredField,clazz.newInstance(),val);
//                    setFieldData(declaredField,bean,val);
                    buildMethod(clazz,bean,declaredField,val);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
//        mySpringContext.refresh(bean.getClass());
    }

    private void setFieldData(Field field, Object bean, String data) throws Exception {
        // 注意这里要设置权限为true
        field.setAccessible(true);
        Class<?> type = field.getType();
        if (type.equals(String.class)) {
            field.set(bean, data);
        } else if (type.equals(Integer.class)) {
            field.set(bean, Integer.valueOf(data));
        } else if (type.equals(Long.class)) {
            field.set(bean, Long.valueOf(data));
        } else if (type.equals(Double.class)) {
            field.set(bean, Double.valueOf(data));
        } else if (type.equals(Short.class)) {
            field.set(bean, Short.valueOf(data));
        } else if (type.equals(Byte.class)) {
            field.set(bean, Byte.valueOf(data));
        } else if (type.equals(Boolean.class)) {
            field.set(bean, Boolean.valueOf(data));
        } else if (type.equals(Date.class)) {
            field.set(bean, new Date(Long.valueOf(data)));
        }
    }

    private String getVal(String key){
        if (StringUtils.isNotBlank(key)){
            return applicationConfigContext.getValue(key);
        }else {
            return applicationConfigContext.getGroup();
        }
    }

    private void buildMethod(Class<?> clz ,Object obj,Field field,String propertiedValue) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        // 获取属性的名字
        String name = field.getName();
        // 将属性的首字符大写, 构造get,set方法
        name = name.substring(0, 1).toUpperCase() + name.substring(1);
        // 获取属性的类型
        String type = field.getGenericType().toString();
        // 如果type是类类型,则前面包含"class ",后面跟类名
        // String 类型
        if (type.equals("class java.lang.String")) {
            Method m = clz.getMethod("set" + name, String.class);
            // invoke方法传递实例对象,因为要对实例处理,而不是类
            m.invoke(obj, propertiedValue);
        }
        // int Integer类型
        if (type.equals("class java.lang.Integer")) {
            Method m = clz.getMethod("set" + name, Integer.class);
            m.invoke(obj, Integer.parseInt(propertiedValue));
        }
        if (type.equals("int")) {
            Method m = clz.getMethod("set" + name, int.class);
            m.invoke(obj, (int) Integer.parseInt(propertiedValue));
        }
        // boolean Boolean类型
        if (type.equals("class java.lang.Boolean")) {
            Method m = clz.getMethod("set" + name, Boolean.class);
            if (propertiedValue.equalsIgnoreCase("true")) {
                m.invoke(obj, true);
            }
            if (propertiedValue.equalsIgnoreCase("false")) {
                m.invoke(obj, true);
            }
        }
        if (type.equals("boolean")) {
            Method m = clz.getMethod("set" + name, boolean.class);
            if (propertiedValue.equalsIgnoreCase("true")) {
                m.invoke(obj, true);
            }
            if (propertiedValue.equalsIgnoreCase("false")) {
                m.invoke(obj, true);
            }
        }
        // long Long 数据类型
        if (type.equals("class java.lang.Long")) {
            Method m = clz.getMethod("set" + name, Long.class);
            m.invoke(obj, Long.parseLong(propertiedValue));
        }
        if (type.equals("long")) {
            Method m = clz.getMethod("set" + name, long.class);
            m.invoke(obj, Long.parseLong(propertiedValue));
        }
        // 时间数据类型
        if (type.equals("class java.util.Date")) {
            Method m = clz.getMethod("set" + name, java.util.Date.class);
            m.invoke(obj, DataConverter.convert(propertiedValue));
        }
    }
}

使用方式demo类

@RestController
@RequestMapping("/version")
@Api(tags = "版本")
@ApiSort(value = 0)
@Data
public class VersionController {
    
    @MyConfig("opcl.url")
    public String url = "1";
    
    @GetMapping(value="/test", produces = "application/json;charset=utf-8")
    @MyConfig(clazz = VersionController.class)
    public Object test(){
        return url;
    }

}

这里如果想在VersionController 注入配置url,首先需要在配置url上添加注解MyConfig,value为配置在容器中的key;其次需要在使用url的方法test上添加注解MyConfig,并将当前class传入,当调用此方法,便会触发aop机制,更新url的值

开发过程遇到的问题

简述

在aop中我使用几种方式进行修改对象的属性。

最终是是第三种证实修改成功。首先spring的bean都是采用动态代理的方式产生。而默认的都是采用单例模式。所以我们需要搞清楚:

versioncontroller方法中拿取url这个属性时,拿取者是谁,是VersionController还是spring进行cglib动态代理产生的bean(以下简称bean)?

这里可以看到Versioncontroller的方法执行时,这里的this是Versioncontroller@9250,这其实代表着是对象本身而非代理对象。后面我们会看到,springbean其实是代理对象代理了被代理对象,执行了其(Versioncontroller)方法。

我们的目的是修改什么?是修改VersionController还是这个bean?

我们讲到,springbean其实是代理对象代理了被代理对象,执行了其(Versioncontroller)方法。那么我们修改的理所应该是被代理对象的属性值。

当进行反射赋值的时候,我们修改的是VersionController这个类还是bean?

首先上面已经明确,修改的应该是被代理对象的属性值。
我这里三种方法。第一种只修改一个新建对象的实例,很明显与springbean理念相悖,不可能实现我们的需求,所以只谈后两种。

先看第二种是通过工具类获取bean,然后通过反射为对应的属性赋值。
这里写一个testController便于验证。

package com.ai.api.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/test")
public class TestController {
    @Autowired
    VersionController versionController;

    @GetMapping("/1")
    public Object getUrl(){
        System.out.println(versionController.getUrl());
        System.out.println(versionController.url);
        return versionController.getUrl();
    }
}

这里我们是直接为bean的属性赋值。我们先调用VersionController中的test方法,让其先走一遍Aop。因为springbean如果没有配置,默认的都是单例模式,所以说如果修改成功,那么testController中,注入的VersionController,因为是同一个VersionController的实例,它的代理对象一定也被修改。我们调试后得出:

我们可以看到,我们确实修改掉了bean的值,但被代理对象的url仍然是1。并没有实现我们想要的效果。

第三种,通过获取这个bean,通过这个代理bean的set方法,间接修改被代理对象VersionController的属性值。我们先调用VersionController中的test方法,让其先走一遍Aop,因为springbean如果没有配置,默认的都是单例模式。如果修改成功,那么testController中,注入的VersionController,因为是同一个VersionController的实例,它的代理对象一定也被修改了。
我们调用TestController 方法可以看到:

这里我们可以看到,被代理的对象已经被成功修改,大功告成!


java/springboot 自定义注解实现 AOP

java/springboot 自定义注解实现 AOP

 

java 注解

即是注释了,百度解释:也叫元数据。一种代码级别的说明。 个人理解:就是内容可以被代码理解的注释,一般是一个类。

元数据

也叫元注解,是放在被定义的一个注解类的前面 ,是对注解一种限制。

谈下这两个: @Retention 和 @Target  

@Retention :用来说明该注解类的生命周期。它有以下三个参数:

RetentionPolicy.SOURCE  : 注解只保留在源文件中

RetentionPolicy.CLASS  : 注解保留在 class 文件中,在加载到 JVM 虚拟机时丢弃

RetentionPolicy.RUNTIME  : 注解保留在程序运行期间,此时可以通过反射获得定义在某个类上的所有注解。

@Target :  用来说明该注解可以被声明在那些元素之前。

ElementType.TYPE:说明该注解只能被声明在一个类前。

ElementType.FIELD:说明该注解只能被声明在一个类的字段前。

ElementType.METHOD:说明该注解只能被声明在一个类的方法前。

ElementType.PARAMETER:说明该注解只能被声明在一个方法参数前。

ElementType.CONSTRUCTOR:说明该注解只能声明在一个类的构造方法前。

ElementType.LOCAL_VARIABLE:说明该注解只能声明在一个局部变量前。

ElementType.ANNOTATION_TYPE:说明该注解只能声明在一个注解类型前。

ElementType.PACKAGE:说明该注解只能声明在一个包名前。

 

实现自定义注解 AOP:

LogAnnotation.java

package com.pupeiyuan.aop;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import javax.persistence.Inheritance;

@Documented//说明该注解将被包含在javadoc中
@Retention(RetentionPolicy.RUNTIME)// 注解会在class字节码文件中存在,在运行时可以通过反射获取到
@Target(ElementType.METHOD)
@Inheritance//说明子类可以继承父类中的该注解
public @interface LogAnnotation {

    String value() default "-----AOP拦截执行完毕!----";
}

 

WebLogAspect.java

package com.pupeiyuan.aop;

import java.lang.reflect.Method;

import javax.servlet.http.HttpServletRequest;
import org.apache.log4j.Logger;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

@Aspect
@Component
public class WebLogAspect {

    protected static final Logger logger = Logger.getLogger(WebLogAspect.class);
    
    //定义一个切入点
    @Pointcut("@annotation(com.pupeiyuan.aop.LogAnnotation)")
    public void annotationPointCut(){
        
    }
    @Before("annotationPointCut()")
    public void doBefore(JoinPoint joinPoint) {
        
        MethodSignature methodSignature = (MethodSignature)joinPoint.getSignature();
        Method method = methodSignature.getMethod();
        LogAnnotation annotation = method.getAnnotation(LogAnnotation.class);
        // 记录请求到达时间
         //接收到请求,记录请求内容
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();

        // 记录下请求内容
        logger.info("URI : " + request.getRequestURI());
        logger.info("URL : " + request.getRequestURL());
        logger.info("HTTP_METHOD : " + request.getMethod());
        logger.info("IP : " + request.getRemoteAddr());
        logger.info(annotation.value());
    }
}

 

controller

package com.pupeiyuan.controller;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

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

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.ModelAndView;

import com.github.pagehelper.PageHelper;
import com.pupeiyuan.aop.LogAnnotation;
import com.pupeiyuan.bean.NhReportStatusHistory;
import com.pupeiyuan.common.controller.BaseController;
import com.pupeiyuan.core.DataSourceKey;
import com.pupeiyuan.core.DynamicDataSourceContextHolder;
import com.pupeiyuan.services.NhReportService;
/**
 * @author pypua
 * @date 2018年8月30日 上午9:21:20
 * 
 */
@Controller
@RequestMapping("burket")
@Scope("prototype")
public class BurketController extends BaseController {
    
    //services层注入
    @Autowired NhReportService nhReportService;
    @LogAnnotation
    @RequestMapping(value = "/burketList", method = {RequestMethod.GET,RequestMethod.POST})
    public ModelAndView burketList(HttpServletRequest request,
            HttpServletResponse response
            ) throws Exception {
        System.out.println("hello,springboot");
       //参数容器
        Map<String, Object> params = new HashMap<String, Object>();
        PageHelper.startPage(1, 2);
        DynamicDataSourceContextHolder.set(DataSourceKey.DB_SLAVE1);
        List<NhReportStatusHistory> findList = nhReportService.findList(params);
        ModelAndView modelAndView = new ModelAndView(); 
        modelAndView.setViewName("burketList");
        modelAndView.addObject("list", findList);
        return modelAndView;
    }
    
}

 

效果

spring 拦截器 ---- 方法执行之前 ---------
2018-12-07 16:48:53.769 INFO 92292 --- [nio-8082-exec-4] com.pupeiyuan.aop.WebLogAspect : URI : /burket/burketList
2018-12-07 16:48:53.778 INFO 92292 --- [nio-8082-exec-4] com.pupeiyuan.aop.WebLogAspect : URL : http://localhost:8082/burket/burketList
2018-12-07 16:48:53.778 INFO 92292 --- [nio-8082-exec-4] com.pupeiyuan.aop.WebLogAspect : HTTP_METHOD : GET
2018-12-07 16:48:53.778 INFO 92292 --- [nio-8082-exec-4] com.pupeiyuan.aop.WebLogAspect : IP : 0:0:0:0:0:0:0:1
2018-12-07 16:48:53.778 INFO 92292 --- [nio-8082-exec-4] com.pupeiyuan.aop.WebLogAspect : -----AOP 拦截执行完毕!----
hello,springboot
2018-12-07 16:48:53.788 INFO 92292 --- [nio-8082-exec-4] c.p.core.DynamicRoutingDataSource : 当前数据源:{} DB_SLAVE1

java基础复习-自定义注解3(自定义注解在SpringBoot中的使用)

java基础复习-自定义注解3(自定义注解在SpringBoot中的使用)

#java基础复习-自定义注解3(自定义注解在SpringBoot中的使用) ##写在前面: ###1、本节内容源于前些日子工作的真实业务情况,为了方便本节叙述,特地将公司的项目单独宅出来作为讲解。 ###2、当时做该项目的开发时,有一个记录日志的需求,当时的第一想法是利用拦截器去完成,但是却也有着一些不方便的地方,因此使用了自定义注解技术进行了改进。 ###3、本节涉及的知识有自定义注解、SpringBoot框架、mybatis技术,spring切面的知识,如果未曾了解过该知识,可以先行进行学习。 ##1、搭建演示环境 ###1.1、数据库日志表的搭建: 建日志表的SQL语句:

DROP TABLE IF EXISTS `t_log`;
CREATE TABLE `t_log`  (
  `logID` bigint(20) NOT NULL AUTO_INCREMENT COMMENT ''日志ID 主键自增长'',
  `operator` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT ''操作人'',
  `opDescribe` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT ''操作描述'',
  `operMethod` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT ''操作方法'',
  `opAddress` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT ''操作地址'',
  `operTime` datetime(0) DEFAULT NULL COMMENT ''操作时间'',
  `delFlag` int(11) NOT NULL DEFAULT 0 COMMENT ''置废标识'',
  UNIQUE INDEX `logID`(`logID`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 337176 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

日志表的截图: 该表t_log各个字段的含义见上述sql语句中的备注字段 ###1.2、项目的目录结构 ###1.3、pom.xml的内容 <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.7.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.xgp</groupId> <artifactId>mylog</artifactId> <version>0.0.1-SNAPSHOT</version> <name>mylog</name> <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.0</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

其中注意,与一般的SpringBoot项目中,此处多引入了一个依赖(Spring框架的apo依赖),因为本次演示需要使用到Spring框架的切面。

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

###1.4、配置文件(application.yml) spring: datasource: # 数据源基本配置 username: **** password: **** driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://localhost:3307/smartclassroom?useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=Asia/Shanghai&zeroDateTimeBehavior=round

server:
  port: 80

#开启mybatis的sql语句显示功能
logging:
  level:
    com:
      xgp:
        mylog:
          mapper: debug

###1.5、准备日志表的实体类 package com.xgp.mylog.model;

import java.util.Date;

public class Log {
    
    private Long logid;
    private String operator;
    private String opdescribe;
    private String opermethod;
    private String opaddress;
    private Date opertime;
    private Integer delflag;

    public Log() {
    }

    public Log(String operator, String opdescribe, String opermethod, String opaddress, Date opertime) {
        this.operator = operator;
        this.opdescribe = opdescribe;
        this.opermethod = opermethod;
        this.opaddress = opaddress;
        this.opertime = opertime;
    }

    public Long getLogid() {
        return logid;
    }
    public void setLogid(Long logid) {
        this.logid = logid;
    }
    public String getOperator() {
        return operator;
    }
    public void setOperator(String operator) {
        this.operator = operator == null ? null : operator.trim();
    }

    public String getOpdescribe() {
        return opdescribe;
    }

    public void setOpdescribe(String opdescribe) {
        this.opdescribe = opdescribe == null ? null : opdescribe.trim();
    }

    public String getOpermethod() {
        return opermethod;
    }

    public void setOpermethod(String opermethod) {
        this.opermethod = opermethod == null ? null : opermethod.trim();
    }

    public String getOpaddress() {
        return opaddress;
    }

    public void setOpaddress(String opaddress) {
        this.opaddress = opaddress == null ? null : opaddress.trim();
    }

    public Date getOpertime() {
        return opertime;
    }

    public void setOpertime(Date opertime) {
        this.opertime = opertime;
    }

    public Integer getDelflag() {
        return delflag;
    }

    public void setDelflag(Integer delflag) {
        this.delflag = delflag;
    }

    @Override
    public String toString() {
        return "Log{" +
                "logid=" + logid +
                ", operator=''" + operator + ''\'''' +
                ", opdescribe=''" + opdescribe + ''\'''' +
                ", opermethod=''" + opermethod + ''\'''' +
                ", opaddress=''" + opaddress + ''\'''' +
                ", opertime=" + opertime +
                ", delflag=" + delflag +
                ''}'';
    }
}

##2、编写自定义注解 ###2.1、明确注解的作用访问:因为是记录请求日志的注解,所以应该作用在方法上 @Target(ElementType.METHOD) ###2.2、明确使用阶段:运行时 @Retention(RetentionPolicy.RUNTIME) ###2.3、明确注解的参数:根据t_log数据表,其中的请求方法的中文名称是需要在编写各个请求方法时进行写入的,而不能系统自动生成,因此注解的请求参数只有一个。完整的自定义注解的代码为: package com.xgp.mylog.annotation;

import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyLog {
    String value();
}

##3、编写注解解析器 ###3.1、Spring框架的Aop切面的介绍使用 如果要使用Spring框架的Aop切面,需要在类级别上标注@Aspect注解

如果该注解标红,是你前面的依赖没导 ###使用Spring的Aop切面,有三个关键的注解 切点注解:

@Pointcut("@annotation(com.xgp.mylog.annotation.MyLog)")

表示在切点前进行解析自定义注解的注解:

@Before("annotationPointcut()")

表示在切点后进行解析自定义注解的注解:

@After("annotationPointcut()")

本次的日志因为需要写入数据库,而与数据库的交互的方法较慢,为了不影响用户的体验速度,因此是在操作完成后进行日志的写入。 ###3.2、定义注解解析器类,并注册成为Spring的组件 @Aspect @Component public class MyLogAspect { ###3.3、因为设计与数据库的交互,因此需要注入Mapper

@SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
@Autowired
LogMapper logMapper;

###3.4、编写切点函数 @Pointcut("@annotation(com.xgp.mylog.annotation.MyLog)") public void annotationPointcut() {

}

###3.5、编写在切点后进行解析注解的函数 @After("annotationPointcut()") public void afterPointcut(JoinPoint joinPoint) throws IOException {

其中该方法的JoinPoint参数为切点对象类,可以通过该类的实例获得被切方法的一些信息。 ###3.6、拿到注解上的value的值,即为请求方法的中文解释 MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); Method method = methodSignature.getMethod(); MyLog annotation = method.getAnnotation(MyLog.class); String opdescribe = annotation.value(); ###3.7、请求方法的中文值为由方法的编写者写入的,想要获取到其他信息,还需要获取到请求的request对象 ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = attributes.getRequest(); ###3.8、通过请求的request对象获取请求的方法名 String[] uri = request.getRequestURI().split("/"); String requestName = uri[uri.length - 1]; ###3.9、判断改用户是否进行了登陆,如果没有进行登陆,则为登陆方法,从传递过来的参数中来获取操作人。 String operator = null; if(request.getSession().getAttribute("token") == null) { operator = (String) joinPoint.getArgs()[0]; } ###3.10、编写获取请求的ip地址的函数,并获取请求方法来源的ip地址 String ip = getIpAddr(request);

该函数如下:

public  String getIpAddr(HttpServletRequest request) {
    String ip = request.getHeader("x-forwarded-for");
    if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
        ip = request.getHeader("Proxy-Client-IP");
    }
    if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
        ip = request.getHeader("WL-Proxy-Client-IP");
    }
    if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
        ip = request.getRemoteAddr();
    }
    if ("0:0:0:0:0:0:0:1".equals(ip)) {
        ip = "127.0.0.1";
    }
    if (ip.split(",").length > 1) {
        ip = ip.split(",")[0];
    }
    return ip;
}

###3.11、自动生成日志时间,封装对象

    Date requestTime = new Date();
    Log log = new Log();
    log.setOperator(operator);
    log.setOpdescribe(opdescribe);
    log.setOpermethod(requestName);
    log.setOpaddress(ip);
    log.setOpertime(requestTime);

###3.12、判断该用户是否登陆成功,若登陆成功,则写入数据库。防治恶意登陆进行数据库爆库。 //获取返回结果 int result = (int) request.getAttribute("flag"); if(result == 1) { //登陆成功写入数据库 logMapper.insert(log); } ###3.13、该注解解析器的网整的代码如下:

package com.xgp.mylog.annotation.aspect;

import com.xgp.mylog.annotation.MyLog;
import com.xgp.mylog.mapper.LogMapper;
import com.xgp.mylog.model.Log;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.Date;

@Aspect
@Component
public class MyLogAspect {

    @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
    @Autowired
    LogMapper logMapper;

    @Pointcut("@annotation(com.xgp.mylog.annotation.MyLog)")
    public void annotationPointcut() {

    }

    @After("annotationPointcut()")
    public void afterPointcut(JoinPoint joinPoint) throws IOException {
        MethodSignature methodSignature =  (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();
        MyLog annotation = method.getAnnotation(MyLog.class);
        String opdescribe = annotation.value();

        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();

        String[] uri = request.getRequestURI().split("/");
        String requestName = uri[uri.length - 1];

        String operator = null;
        if(request.getSession().getAttribute("token") == null) {
             operator = (String) joinPoint.getArgs()[0];
        }

        String ip = getIpAddr(request);

        Date requestTime = new Date();
        Log log = new Log();
        log.setOperator(operator);
        log.setOpdescribe(opdescribe);
        log.setOpermethod(requestName);
        log.setOpaddress(ip);
        log.setOpertime(requestTime);

        //获取返回结果
        int result = (int) request.getAttribute("flag");
        if(result == 1) {
            //登陆成功写入数据库
            logMapper.insert(log);
        }

    }

    public  String getIpAddr(HttpServletRequest request) {
        String ip = request.getHeader("x-forwarded-for");
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        if ("0:0:0:0:0:0:0:1".equals(ip)) {
            ip = "127.0.0.1";
        }
        if (ip.split(",").length > 1) {
            ip = ip.split(",")[0];
        }
        return ip;
    }
}

##4、编写插入一条日志的Mapper 注解解析器中设计到了日志与数据库的交互,因此在该类中编写与数据库交互的方法:

package com.xgp.mylog.mapper;

import com.xgp.mylog.model.Log;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface LogMapper {

    @Insert("insert into t_log (operator,opDescribe,operMethod,opAddress,operTime) values (#{operator,jdbcType=VARCHAR}, #{opdescribe,jdbcType=VARCHAR}, #{opermethod,jdbcType=VARCHAR}, #{opaddress,jdbcType=VARCHAR}, #{opertime,jdbcType=TIMESTAMP})")
    void insert(Log log);
}

这里需注意一点,mybatis在取方法的参数值时,最好以#{}d的方式进行取值,可以有效的防治SQL注入的问题。 ##5、编写测试的Controller,这里以登陆日志作为演示 package com.xgp.mylog.Controller;

import com.xgp.mylog.annotation.MyLog;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;

@RestController
public class UserController {

    /**
     * 登陆方法
     */
    @MyLog("登陆方法")
    @GetMapping("/login")
    public String login(@RequestParam String username, @RequestParam String password, HttpServletRequest request) {
        if("xgp".equals(username) && "123".equals(password)) {
            request.setAttribute("flag",1);
            return "登陆成功";
        }
        request.setAttribute("flag",0);
        return "登陆失败";
    }
}

这里使用自己的自定义注解上的参数为"登陆方法",并且如果登陆成功,则像request域中写入1,反之写入0。 ##6、进行测试 因为方便测试,这里的登陆方法采用的是GET请求,测试者可以在浏览器中的地址栏中方便的进行测试: 看到前端返回了登陆成功,表示我们的数据已经成功的插如到了数据库中,打开数据库中进行验证,也确实插入成功了。 ##7、与用拦截器写日志的方法进行对比 之前使用拦截器的做法:

package com.c611.smartclassroom.component;

import com.c611.smartclassroom.mapper.LogMapper;
import com.c611.smartclassroom.model.Log;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.ServletOutputStream;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

public class LogHandlerInterceptor implements HandlerInterceptor {

    @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
    @Autowired
    LogMapper logMapper;

   private static Map<String,String> requestMap = new HashMap<>();

   public LogHandlerInterceptor() {
//       requestMap.put("querySchByClassSeme","查询课表");
       requestMap.put("exportSch","导出课表");
       requestMap.put("importSch","导入课表");
       requestMap.put("addSchool","添加学校");
       requestMap.put("addZone","添加区域");
       requestMap.put("addBuild","添加教学楼");
       requestMap.put("addClassRoom","添加教室");
       requestMap.put("setCourseTime","设置课时");
       requestMap.put("setSemesterDate","设置学期时间");
       requestMap.put("editSemesterDate","编辑学期时间");
       requestMap.put("addRole","增加角色");
       requestMap.put("delRole","删除角色");
       requestMap.put("editAuth","编辑授权码");

       //工单管理模块
       requestMap.put("queryWorkOrder","按时间排序分页查询所有工单");
       requestMap.put("queryWorkOrderResult","按工单编号查询处理结果");
       requestMap.put("saveWorkOrderResult","填写处理结果");
       requestMap.put("delWorkOrder","删除工单");
       requestMap.put("addWorkOrder","增加工单");


       //网关管理
       requestMap.put("addGateWay","添加网关");
       requestMap.put("saveGateWay","编辑网关");
       requestMap.put("delGateWay","删除网关");
       //设备管理
       requestMap.put("addDevice","添加设备");
       requestMap.put("saveDevice","编辑设备信息");
       requestMap.put("delDevice","删除设备");

   }

    /*    private static final String USER_AGENT = "user-agent";
    private Logger logger = LoggerFactory.getLogger(this.getClass());*/

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {

/*        HttpSession sesson = request.getSession();
        String userId = (String) sesson.getAttribute("userId");
        //说明未登陆,不能放行
        if(userId == null) return false;
        //查数据库,根据userId查找用户,或者从session中取出*/
        String userName = "薛国鹏";

        String[] uri = request.getRequestURI().split("/");
        String requestName = uri[uri.length - 1];
//        System.out.println(requestName);
        String chaineseName = requestMap.get(requestName);

//        System.out.println(chaineseName);

        if(chaineseName != null) {
            String ip = getIpAddr(request);
            Date requestTime = new Date();
            Log log = new Log();
            log.setOperator(userName);
            log.setOpdescribe(chaineseName);
            log.setOpermethod(requestName);
            log.setOpaddress(ip);
            log.setOpertime(requestTime);
            logMapper.insertSelective(log);
            return true;
        }

        return false;
    }


    public  String getIpAddr(HttpServletRequest request) {
        String ip = request.getHeader("x-forwarded-for");
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        if ("0:0:0:0:0:0:0:1".equals(ip)) {
            ip = "127.0.0.1";
        }
        if (ip.split(",").length > 1) {
            ip = ip.split(",")[0];
        }
        return ip;
    }

}

分析代码可以知道,有如下弊端:

  1. 其中有一个Map集合数据量可能会较大,占用较大内存

  2. 拦截器的编写者需要手动的将其他开发者编写的Controller层的方法一一翻译放入map中,工作量大且繁琐。

spring boot aop 打印出入参配置(自定义注解)

spring boot aop 打印出入参配置(自定义注解)

DB 操作出入参日志打印 AOP 配置

 

背景:目前项目做中台建设,要求统一打印 DB 操作的出入参,并以 json 格式输出。

 

  1. 方案描述:

 

基于 Spring Boot 项目,通过自定义注解,使用 AOP 对自定义注解配置前置通知、后置通知对参数进行处理。日志处理使用 slf4j 处理,需要打印日志的类只需添加 @Slf4j 注解。@Profile ({"dev", "test",prod}) 可以配置在配置切面的类上,这样可以选择在哪个环境使用该切面配置。

 

  1. 实施步骤:
  1. Spring 项目添加 maven 依赖 spring-boot-starter-aop 注入 AOP 功能,添加 gson 依赖注入 json 输出功能。

<dependency>

        <groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-aop</artifactId>

</dependency>

<dependency>

    <groupId>com.google.code.gson</groupId>

    <artifactId>gson</artifactId>

    <version>2.8.5</version>

</dependency>

 

  1. 自定义注解,如下定义

 

@Retention(RetentionPolicy.RUNTIME)

@Target({ElementType.METHOD})

@Documentedpublic @interface DBLog {

    String description() default "";

}

 

  1. 配置切面

 

c1、配置切点:创建 DBLogAspect 类,使用 @Pointcut 注解声明切点。如 下代码

@Pointcut(“DBLog ”)

Public void dbParameterLog(){

//empty

}

c2、定义 @Around 环绕通知,用于记录执行时间。如下代码

@Around("dbParameterLog()")

    public Object doAround(ProceedingJoinPoint pjp) throws Throwable {

        long startTime = System.currentTimeMillis();

        Object ob = pjp.proceed ();//ob 为方法的返回值

   logger.info("Response Args  : {}", new Gson().toJson(result));

        logger.info ("耗时 :" + (System.currentTimeMillis () - startTime));

        return ob;

}

 

C3、定义 @Before 通知打印入参。代码如下

@Before("dbParameterLog()")

     public void doBefore(JoinPoint joinPoint) throws Throwable {

         ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();

        HttpServletRequest request = attributes.getRequest();

   loggger.info ("参数 :" + joinPoint.getArgs ());

    }

 

C4、定义 @AfterReturning 通知打印出参。代码如下

 @AfterReturning(returning = "ret", pointcut = "dbParameterLog()")

     public void doAfterReturning(Object ret) throws Throwable {

         logger.debug ("返回值 :" + JSON.toJSONString (ret));

     }

 

C5、定义 @After 通知打印方法内代码逻辑执行结束。代码如下

@After("webLog()")

     public void doAfter() throws Throwable {

         logger.info("======= End ============" + LINE_SEPARATOR);

     }

 

d、如何使用

DBLog 注解添加到 CRUD 的接口上就可以打印参数。

Spring Boot 自动配置的原理、核心注解以及利用自动配置实现了自定义 Starter 组件

Spring Boot 自动配置的原理、核心注解以及利用自动配置实现了自定义 Starter 组件

本章内容

  1. 自定义属性快速入门
  2. 外化配置
  3. 自动配置
  4. 自定义创建 Starter 组件

摘录:读书是读完这些文字还要好好用心去想想,写书也一样,做任何事也一样

图 2 第二章目录结构图

第 2 章 Spring Boot 配置

Spring Boot 配置,包括自动配置和外化配置。本章先实现自定义属性工程,将属性外化配置在 application.properties 应用配置文件,然后在工程中获取该属性值。接着会详细介绍属性的获取方式、外化配置和自动配置。最后会介绍利用自动配置自定义 Start 组件。

2.1 快速入门工程

第一章的 HelloBookController 控制层中,在代码中以硬编码的方式使用字符串表示书信息。下面把书的信息作为属性,外化配置在 application.properties 。好处是将应用参数、业务参数或第三方参数等统一配置在应用配置文件中,避免配置侵入业务代码,达到可配置的方式,方便及时调整修改。

2.1.1 配置属性

新建工程命名为 chapter-2-spring-boot-config ,在 application.properties 中配置书名和作者,配置如下:

## 书信息
demo.book.name=[Spring Boot 2.x Core Action]
demo.book.writer=BYSocket

.properties 文件的每行参数被存储为一对字符串,即一个存储参数名称,被称为键;另一个为值。一般称为键值对配置。井号(#)或者英文状态下的叹号(!)作为第一行中第一个非空字符来表示该行的文本为注释。另外,反斜杠()用于转义字符。

Spring Boot 支持并推荐使用 YAML 格式的配置文件,将 application.properties 文件替换成 application.yml 文件,并配置相同的属性,配置如下:

## 书信息
demo:
    book:
        name: 《Spring Boot 2.x 核心技术实战 - 上 基础篇》
        writer: 泥瓦匠BYSocket

YAML 是一个可读性高,用来表达数据序列的格式。表示键值对格式时,注意键和值由冒号及空白字符分开。强调下,空白字符是必须的,IDE 一般也会提示。两种配置方式都非常便捷,在开发中选择 .properties 或 .yml 文件配置。但如果两种配置文件同时存在的时候,默认优先使用 .properties 配置文件。YAML 与 .properties 配置文件对比如图 2-1 所示:

图 2-1 YAML 与 .properties 配置文件对比

注意:

在 application.properties 配置中文值,读取时会出现中文乱码问题。因为 Java .properties 文件默认编码方式是 iso-8859 ,Spring Boot 应用以 UTF-8 的编码方式读取,就导致出现乱码问题。

官方 Issue 中的解决方法是,将 .properties 文件中配置的中文值转义成 Unicode 编码形式。例如 demo.book.writer=泥瓦匠 应该配置成 demo.book.writer=\u6ce5\u74e6\u5320 。利用 IDEA properties 插件 或利用 Java 文件转码工具 native2ascii 来快速地进行转义。该工具有在线版实现,地址如下:
https://javawind.net/tools/na...

2.1.2 创建属性类

在工程中新建包目录 demo.springboot.config ,并在目录中创建名为 BookProperties 的属性类,代码如下:

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

/**
 * 书属性
 */
@Component
public class BookProperties {

    /**
     * 书名
     */
    @Value("${demo.book.name}")
    private String name;

    /**
     * 作者
     */
    @Value("${demo.book.writer}")
    private String writer;

    // ... 省略 getter / setter 方法
}

利用 @Component 注解定义了书的属性 Bean,并通过 @Value 注解为该 Bean 的成员变量(或者方法参数)自动注入 application.properties 文件的属性值。@Value 注解是通过 “${propName}” 的形式引用属性,propName 表示属性名称。

核心注解的知识点:

  • @Component 注解:

@Component 对类进行标注,职责是泛指组件 Bean ,应用启动时会被容器加载并加入容器管理。常见的 @Controller@Service@Repository@Component 的分类细化组件,分别对应控制层、服务层、持久层的 Bean。

  • @Value 注解:

@Value 对 Bean 的字段或者方法参数进行标注,职责是基于表达式给字段或方法参数设置默认属性值。通常格式是注解 + SpEL 表达式,如 @Value("SpEL 表达式")

使用 @Vlaue 注解来引用属性值时,确保所引用的属性值在 application.properties 文件存在并且相对应匹配,否则会造成 Bean 的创建错误,引发 java.lang.IllegalArgumentException 非法参数异常。

2.1.3 获取属性

修改原有的 HelloBookController 类,通过注入的方式获取书属性 Bean 并返回。代码如下:

import demo.springboot.config.BookProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloBookController {

    @Autowired
    BookProperties bookProperties;

    @GetMapping("/book/hello")
    public String sayHello() {
        return "Hello, " + bookProperties.getWriter() + " is writing "
                + bookProperties.getName() + " !";
    }
}

通过 @Autowired 注解标记在 BookProperties 字段,控制层自动装配属性 Bean 并使用。默认情况下要求被注解的 Bean 必须存在,需要允许 NULL 值,可以设置其 required 属性为 false: @Autowired(required = false)

2.1.4 运行工程

执行 ConfigApplication 类启动,在控制台看到成功运行的输出后,打开浏览器访问 /book/hello 地址,可以看到如图 2-2 所示的返回结果:

图 2-2 Hello Book 页面

也可以通过单元测试的方式验证属性获取是否成功,单元测试具体相关的会在第 9 章节介绍。单元测试代码如下:

import demo.springboot.config.BookProperties;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@SpringBootTest
public class ConfigApplicationTests {
    @Autowired
    BookProperties bookProperties;

    @Test
    public void testBookProperties() {
        Assert.assertEquals(bookProperties.getName(),"''Spring Boot 2.x Core Action''");
        Assert.assertEquals(bookProperties.getWriter(),"BYSocket");
    }
}

2.2 配置属性的获取方式

配置属性的常用获取方式有基于 @Value@ConfigurationProperties 注解两种方式。两种方式适合的场景不同,下面具体介绍其使用方法和场景。

2.2.1 @Value 注解

@Value 注解对 Bean 的变量或者方法参数进行标注,职责是基于表达式给字段或方法参数设置默认属性值。通常格式是注解 + SpEL 表达式,如 @Value("SpEL 表达式"),并标注在对应的字段或者方法上方,且必须对变量一一标注。这种方式适用于小而不复杂的属性结构。属性结构复杂,字段很多的情况下,这种方式会比较繁琐,应该考虑使用 @ConfigurationProperties 注解。

另外通过 @PropertySource 注解引入对应路径的其他 .properties 文件。将书信息重新配置在 classpath 下新的 book.properties 配置文件后,读取新配置文件的代码如下:

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Component;

/**
 * 书属性
 */
@Component
@PropertySource("classpath:book.properties")
public class BookProperties {

    /**
     * 书名
     */
    @Value("${demo.book.name}")
    private String name;

    /**
     * 作者
     */
    @Value("${demo.book.writer}")
    private String writer;

    // ... 省略 getters / setters 方法
}

2.2.2 @ConfigurationProperties 注解

在包目录 demo.springboot.config 中创建名为 BookComponent 的属性类,并使用 @ConfigurationProperties 注解获取属性,代码如下:

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

/**
 * 书属性
 *
 */
@Component
@ConfigurationProperties(prefix = "demo.book")
public class BookComponent {

    /**
     * 书名
     */
    private String name;

    /**
     * 作者
     */
    private String writer;

    // ... 省略 getters / setters 方法
}

类似 @Value 注解方式,使用 @ConfigurationProperties(prefix = "demo.book") 注解标注在类上方可以达到相同的效果。 @ConfigurationProperties 注解的 prefix 是指定属性的参数名称。会匹配到配置文件中 “ demo.book. ” 结构的属性,星号 “ ” 是指会一一对应匹配 BookComponent 类的字段名。例如,字段 name 表示书名,会匹配到 demo.book.name 属性值。

@Value 注解方式强制字段必须对应在配置文件, @ConfigurationProperties 注解方式则不是必须的。一般情况下,所有字段应该保证一一对应在配置文件。如果没有属性值对应的话,该字段默认为空, @ConfigurationProperties 注解方式也不会引发任何异常,Spring Boot 推荐使用 @ConfigurationProperties 注解方式获取属性。

同样使用单元测试验证获取属性是否成功。单元测试代码如下:

@Autowired
BookComponent bookComponent;

@Test
public void testBookComponent() {
    Assert.assertEquals(bookComponent.getName(),"''Spring Boot 2.x Core Action''");
    Assert.assertEquals(bookComponent.getWriter(),"BYSocket");
}
API org.springframework.boot.context.properties.ConfigurationProperties 注解参数
  • prefix

字符串值,绑定该名称前缀的属性对象。

  • value

字符串值,功能同 prefix 参数。

  • ignoreInvalidFields

布尔值,默认 false。绑定对象时,忽略无效字段。

  • ignoreUnknownFields

布尔值,默认 true。绑定对象时,忽略未知字段。

2.2.3 @ConfigurationProperties 数据验证

@ConfigurationProperties 注解方式支持验证功能,即当属性类被 @Validated 注解标注时,Spring Boot 初始化时会验证类的字段。在类的字段上添加 JSR-303 约束注解,进行数据验证。下面为书属性字段添加非 NULL 和字符串非空约束,代码如下:

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import org.springframework.validation.annotation.Validated;

import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;

/**
 * 书属性
 *
 */
@Component
@ConfigurationProperties(prefix = "demo.book")
@Validated
public class BookComponent {

    /**
     * 书名
     */
    @NotEmpty
    private String name;

    /**
     * 作者
     */
    @NotNull
    private String writer;

    // ... 省略 getters / setters 方法
}

通过 @Validated 注解开启对 BookComponent 类字段的数据验证,如果 name 字段为 NULL 或者为空字符串时,会引发 BindValidationException 绑定数据验证异常。数据验证常用在邮箱格式或者有长度限制的属性字段。另外,验证嵌套属性的值,必须在嵌套对象字段上方标注 @Valid 注解,用来触发其验证。例如,在书属性中新增嵌套对象出版社 Publishing,就需要在该对象上方标注 @Valid 注解,来开启对 Publishing 对象的数据验证。综上,两种属性获取方式各有优缺点,对比如图 2-3 所示:

图 2-3 @ConfigurationPropertiesd vs @Value

2.3 外化配置

Spring Boot 可以将配置外部化,即分离存储在 classpath 之外,这种模式叫做 “外化配置”。常用在不同环境中,将配置从代码中分离外置,只要简单地修改下外化配置,可以依旧运行相同的应用代码。外化配置表现形式不单单是 .properties 和 .yml 属性文件,还可以使用环境变量和命令行参数等来实现。那么,多处配置了相同属性时,Spring Boot 是通过什么方式来控制外化配置的冲突呢?答案是外化配置优先级。

2.3.1 外化配置优先级

用命令行配置去覆盖 .properties 文件配置方法很简单。正常情况下利用 Java 命令运行工程,代码如下:

// chapter-2-spring-boot-config 目录下运行
java -jar target/chapter-2-spring-boot-config-1.0.jar

下面将书的作者信息 BYSocket 改成 Jeff , 通过命令行配置覆盖属性,代码如下:

java -jar target/chapter-2-spring-boot-config-1.0.jar --demo.book.writer=Jeff

在命令行配置中,设置属性值的格式是用两个连续的减号 “--”标志属性。在控制台看到成功运行的输出后,打开浏览器,访问 /book/hello 地址,可以看到如图 2-4 所示的返回结果:

图 2-4 书信息被覆盖页面

通过命令行配置覆盖属性提供了非常大的作用与便利性,常见于使用 shell 脚本运行工程时,可以方便地修改工程运行的配置。

但是这就引发了一个问题,岂不让工程很有侵入性,如果开放这个功能,导致未知的安全问题。所以 Spring Boot 提供了屏蔽命令行属性值设置,在应用启动类中设置 setAddCommandLineProperties 方法为 false ,用于关闭命令行配置功能,代码如下:

SpringApplication.setAddCommandLineProperties(false);

命令行配置属性的优先级是第四。外化配置获取属性时,会按优先级从高到低获取。如果高优先级存在属性,则返回属性,并忽略优先级低的属性。优先级如下:

  1. 本地 Devtools 全局配置
  2. 测试时 @TestPropertySource 注解配置
  3. 测试时 @SpringBootTest 注解的 properties 配置
  4. 命令行配置
  5. SPRING_APPLICATION_JSON 配置
  6. ServletConfig 初始化参数配置
  7. ServletContext 初始化参数配置
  8. Java 环境的 JNDI 参数配置
  9. Java 系统的属性配置
  10. OS 环境变量配置
  11. 只能随机属性的 RandomValuePropertySource 配置
  12. 工程 jar 之外的多环境配置文件(application- {profile}.properties 或 YAML)
  13. 工程 jar 之内的多环境配置文件(application- {profile}.properties 或 YAML)
  14. 工程 jar 之外的应用配置文件(application.properties 或 YAML)
  15. 工程 jar 之内的应用配置文件(application.properties 或 YAML)
  16. @Configuration 类中的 @PropertySource 注解配置
  17. 默认属性配置(SpringApplication.setDefaultProperties 指定)

2.3.2 属性引用

在 application.properties 中配置属性时,属性之间可以直接通过 “${propName}” 的形式引用其他属性。比如新增书的描述 description 属性,代码如下:

## 书信息
demo.book.name=[Spring Boot 2.x Core Action]
demo.book.writer=BYSocket
demo.book.description=${demo.book.writer}''s${demo.book.name}

demo.book.description 属性引用了前面定义的 demo.book.name 和 demo.book.writer 属性,其值为 BYSocket''s[Spring Boot 2.x Core Action] 。一方面可以使相同配置可以复用,另一方面增强了配置的阅读性。

2.3.3 使用随机数

在 application.properties 中配置属性时,可以使用随机数配置,例如注入某些密钥、UUID 或者测试用例,需要每次不是一个固定的值。RandomValuePropertySource 类随机提供整形、长整形数、UUID 或者字符串。使用代码如下:

my.secret=${random.value}
my.number=${random.int}
my.bignumber=${random.long}
my.uuid=${random.uuid}
my.number.less.than.ten=${random.int(10)}
my.number.in.range=${random.int[1024,65536]}

2.3.4 多环境配置

多环境是指不同配置的生产服务器使用同一工程代码部署,比如:开发环境、测试环境、预发环境、生产环境等。各个环境的工程端口、数据库配置、Redis 配置、日志配置等都会不同,传统模式下需要修改配置,工程重新编译打包,并部署到指定环境服务器。结果是容易发生配置错误,导致开发部署效率低下。Spring Boot 使用多环境配置去解决这个问题。

多环境配置,类似 Maven 构建配置文件的思路,即配置多个不同环境的配置文件,再通过 spring.profiles.active 命令去指定读取特定配置文件的属性。多环境配置文件是不同于 application.properties 应用配置文件。多环境配置文件的约定命名格式为 application-{profile}.properties。多环境配置功能默认为激活状态,如果其他配置未被激活,则 {profile} 默认为 default,会加载 application-default.properties 默认配置文件,没有该文件就会加载 application.properties 应用配置文件。

多环境配置文件的属性读取方式和从 application.properties 应用配置文件读取方式一致。不管多环境配置文件在工程 jar 包内还是包外,按照配置优先级覆盖其他配置文件。在微服务实践开发中,经常会使用一个类似 deploy 工程去管理配置文件和打包其他业务工程。

在 application.properties 同级目录中,新建 application-dev.properties 作为开发环境配置文件,配置如下:

## 书信息
demo.book.name=[Spring Boot 2.x Core Action]  From Dev
demo.book.writer=BYSocket

新建 application-prod.properties 作为生产环境配置文件,代码如下:

## 书信息
demo.book.name=<Spring Boot 2.x Core Action Dev> From Prod
demo.book.writer=BYSocket

通过命令行指定读取 dev 环境配置文件并运行工程,代码如下:

java -jar target/chapter-2-spring-boot-config-1.0.jar --spring.profiles.active=dev

在多个环境配置中,通过命令 --spring.profiles.active=dev 指定读取某个配置文件,将 dev 更改成 prod ,轻松切换读取生产环境配置。也可以在控制台的日志中确定配置读取来自 dev :

2017-11-09 12:10:52.978  INFO 72450 --- [           main] demo.springboot.ConfigApplication        : The following profiles are active: dev

最后打开浏览器,访问 /book/hello 地址,可以看到如图 2-5 所示的返回结果:

图 2-5 dev 环境书信息页面

2.4 自动配置

Spring Boot spring-boot-autoconfigure 依赖实现了默认的配置项,即应用默认值。这种模式叫做 “自动配置”。Spring Boot 自动配置会根据添加的依赖,自动加载依赖相关的配置属性并启动依赖。例如默认用的内嵌式容器是 Tomcat ,端口默认设置为 8080。

为什么需要自动配置?顾名思义,自动配置的意义是利用这种模式代替了配置 XML 繁琐模式。以前使用 Spring MVC ,需要进行配置组件扫描、调度器、视图解析器等,使用 Spring Boot 自动配置后,只需要添加 MVC 组件即可自动配置所需要的 Bean。所有自动配置的实现都在 spring-boot-autoconfigure 依赖中,包括 Spring MVC 、Data 和其它框架的自动配置。

2.4.1 spring-boot-autoconfigure 依赖

spring-boot-autoconfigure 依赖,是 Spring Boot 实现自动配置的核心 Starter 组件。其实现源码包结构如图 2-6 所示:

图 2-6 spring-boot-autoconfigure 依赖包目录

从图中可以看出,其中常见核心的包如下:

org.springframework.boot.autoconfigure
org.springframework.boot.autoconfigure.data.jpa
org.springframework.boot.autoconfigure.thymeleaf
org.springframework.boot.autoconfigure.web.servlet
org.springframework.boot.autoconfigure.web.reactive
... 省略

在各自包目录下有对应的自动配置类,代码如下:

JpaRepositoriesAutoConfiguration 
ThymeleafAutoConfiguration
WebMvcAutoConfiguration
WebFluxAutoConfiguration
... 省略

上面自动配置类依次是 Jpa 自动配置类、Thymeleaf 自动配置类、Web MVC 自动配置类和 WebFlux 自动配置类。WebFlux 响应式框架会在第 3 章 详细介绍使用。

spring-boot-autoconfigure 职责是通过 @EnableAutoConfiguration 核心注解,扫描 ClassPath 目录中自动配置类对应依赖,并按一定规则获取默认配置并自动初始化所需要的 Bean。在 application.properties 配置文件也可以修改默认配置项,常用配置清单地址如下:

https://docs.spring.io/spring...

2.4.2 @EnableAutoConfiguration 注解

自动配置工作机制是通过 @EnableAutoConfiguration 注解中 @ImportAutoConfigurationImportSelector 自动配置导入选择器类实现的。查阅源码可得具体流程如下:

  • AutoConfigurationImportSelector 通过 SpringFactoriesLoader.loadFactoryNames() 核心方法读取 ClassPath 目录下面的 META-INF/spring.factories 文件。
  • spring.factories 文件中配置的 Spring Boot 自动配置类,例如常见的 WebMvcAutoConfiguration Web MVC 自动配置类和ServletWebServerFactoryAutoConfiguration 容器自动配置类 。
  • spring.factories 文件和 application.properties 文件都属于配置文件,配置的格式均为键值对。里面配置的每个自动配置类都会定义相关 Bean 的实例配置,也会定义什么条件下自动配置和哪些 Bean 被实例化。
  • 当 pom.xml 添加某 Starter 依赖组件的时候,就会自动触发该依赖的默认配置。

例如添加 spring-boot-starter-web 依赖后,启动应用会触发容器自动配置类。容器自动配置类 ServletWebServerFactoryAutoConfiguration 的部分代码如下:

package org.springframework.boot.autoconfigure.web.servlet;

@Configuration
@ConditionalOnClass({ServletRequest.class})
@ConditionalOnWebApplication(
    type = Type.SERVLET
)
@EnableConfigurationProperties({ServerProperties.class})
@Import({ServletWebServerFactoryAutoConfiguration.BeanPostProcessorsRegistrar.class})
public class ServletWebServerFactoryAutoConfiguration {
... 省略
}

上面代码中,@ConditionalOnClass 注解表示对应的 ServletRequest 类在 ClassPath 目录下面存在,并且 @ConditionalOnWebApplication 注解表示该应用是 Servlet Web 应用时,才会去启动容器自动配置,并通过 ServerProperties 类默认设置了端口为 8080。Type.SERVLET 枚举代表 Servlet Web 应用,Type.REACTIVE 枚举代表响应式 WebFlux 应用。

自动配置,是一把双刃剑。用好了就像天下武功唯快不破一样。但要注意一些自动化配置造成的问题。常见的问题有:

  • Spring Boot 工程添加某些 Starter 组件依赖,又不需要触发组件自动配置
  • Spring Boot 配置多个不同数据源配置时,使用 XML 配置多数据源,但其默认数据源配置会触发自动配置出现问题。

类似场景下,解决方式是通过 exclude 属性指定并排除自动配置类,代码如下:

@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})

也等价于配置 @EnableAutoConfiguration 注解,代码如下:

@SpringBootApplication
@EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class})

自动配置会最大的智能化,只有配置了 exclude 属性时,Spring Boot 优先初始化用户定义的 Bean ,然后再进行自动配置。

2.4.3 利用自动配置自定义 Starter 组件

当公司需要共享或者开源 Spring Boot Starter 组件依赖包,就可以利用自动配置自定义 Starter 组件。一个完整的 Starter 组件包括以下两点:

  • 提供自动配置功能的自动配置模块。
  • 提供依赖关系管理功能的组件模块,即封装了组件所有功能,开箱即用。

实现自定义 Starter 组件,并不会将这两点严格区分,可以将自动配置功能和依赖管理结合在一起实现。下面利用自动配置实现自定义 Starter 组件:spring-boot-starter-swagger 组件是用来快速生成 API 文档,简化原生使用 Swagger2 。

spring-boot-starter-swagger 组件为 Spring For All 社区(spring4all.com)开源项目,源代码地址是 https://github.com/SpringForA...。

什么是 Swagger2

Swagger2 是 API 最大的开发框架,基于 OpenAPI 规范(OAS),管理了 API 整个生命周期,即从 API 设计到文档,从测试到部署。具体更多了解见其官网,https://swagger.io。

spring-boot-starter-swagger 组件依赖

创建一个新的 Spring Boot 工程,命名为 spring-boot-starter-swagger。在 pom.xml 配置相关

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>${version.swagger}</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>${version.swagger}</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-bean-validators</artifactId>
            <version>${version.swagger}</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.16.12</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>
    

配置中添加了 spring-boot-starter 组件依赖用于自动配置特性,springfox-swagger2 依赖是 Swagger2 框架。

Swagger2 属性配置类 SwaggerProperties

新建名为 SwaggerProperties Swagger2 属性配置类,包含了所有默认属性值。使用该组件时,可以在 application.properties 配置文件配置对应属性项,进行覆盖默认配置。代码如下:

import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.boot.context.properties.ConfigurationProperties;
import springfox.documentation.schema.ModelRef;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

@Data
@ConfigurationProperties("swagger")
public class SwaggerProperties {

    /**是否开启swagger**/
    private Boolean enabled;

    /**标题**/
    private String title = "";
    /**描述**/
    private String description = "";
    /**版本**/
    private String version = "";
    /**许可证**/
    private String license = "";
    /**许可证URL**/
    private String licenseUrl = "";
    /**服务条款URL**/
    private String termsOfServiceUrl = "";

    private Contact contact = new Contact();

    /**swagger会解析的包路径**/
    private String basePackage = "";

    /**swagger会解析的url规则**/
    private List<String> basePath = new ArrayList<>();
    /**在basePath基础上需要排除的url规则**/
    private List<String> excludePath = new ArrayList<>();

    /**分组文档**/
    private Map<String, DocketInfo> docket = new LinkedHashMap<>();

    /**host信息**/
    private String host = "";

    /**全局参数配置**/
    private List<GlobalOperationParameter> globalOperationParameters;

     ... 省略,具体代码见 GitHub
}

@ConfigurationProperties(prefix = "swagger") 标注在类上方是指定属性的参数名称为 swagger。会对应匹配到配置文件中 “ swagger.* ” 结构的属性,例如,字段标题 title 表示标题,会匹配到 swagger.title 属性值。

Swagger2 自动配置类 SwaggerAutoConfiguration

新建名为 SwaggerAutoConfiguration Swagger2 自动配置类,提供 Swagger2 依赖关系管理功能和自动配置功能。代码如下:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;

@Configuration
@ConditionalOnProperty(name = "swagger.enabled", matchIfMissing = true)
@Import({
        Swagger2DocumentationConfiguration.class,
        BeanValidatorPluginsConfiguration.class
})
public class SwaggerAutoConfiguration implements BeanFactoryAware {

    private BeanFactory beanFactory;

    @Bean
    @ConditionalOnMissingBean
    public SwaggerProperties swaggerProperties() {
        return new SwaggerProperties();
    }

    @Bean
    @ConditionalOnMissingBean
    @ConditionalOnProperty(name = "swagger.enabled", matchIfMissing = true)
    public List<Docket> createRestApi(SwaggerProperties swaggerProperties) {
        ... 省略,具体代码见 GitHub
    }

    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        this.beanFactory = beanFactory;
    }
}

上面代码实现流程如下:

  1. @Configuration 注解标注在类上方,表明该类为配置类。
  2. @Import 注解引入 Swagger2 提供的配置类 Swagger2DocumentationConfiguration 和 Bean 数据验证插件配置类 BeanValidatorPluginsConfiguration
  3. @ConditionalOnMissingBean 注解标注了两处方法,当 Bean 没有被创建时会执行被标注的初始化方法。第一处被标记方法是 swaggerProperties() ,用来实例化属性配置类 SwaggerProperties;第二处被标记方法是 createRestApi(), 用来实例化 Swagger2 API 映射的 Docket 列表对象。
  4. @ConditionalOnProperty 注解标注在 createRestApi() 方法,name 属性会去检查环境配置项 swagger.enabled 。默认情况下,属性存在且不是 false 的情况下,会触发该初始化方法。matchIfMissing 属性默认值为 false ,这里设置为 true,表示如果环境配置项没被设置,也会触发。
Swagger2 启动注解类 EnableSwagger2Doc

新建名为 EnableSwagger2Doc Swagger2 启动注解类,用于开关 spring-boot-starter-swagger 组件依赖。代码如下:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import({SwaggerAutoConfiguration.class})
public @interface EnableSwagger2Doc {


}

上面代码 @Import 注解引入 Swagger2 自动配置类 SwaggerAutoConfiguration。当将该注解配置在应用启动类上方,即可开启 Swagger2 自动配置及其功能。

使用 spring-boot-starter-swagger 组件依赖

上面简单介绍了spring-boot-starter-swagger 组件的核心代码实现,同样使用方式也很简单。在 chapter-2-spring-boot-config 工程的 Maven 配置中添加对应的依赖配置,目前支持 1.5.0.RELEASE 以上版本,配置如下:

<!-- 自定义 swagger2 Starter 组件依赖 -->
<dependency>
    <groupId>com.spring4all</groupId>
    <artifactId>spring-boot-starter-swagger</artifactId>
    <version>2.0</version>
</dependency>

另外,需要在 ConfigApplication 应用启动类上方配置启动注解类 EnableSwagger2Doc,代码如下:

import com.spring4all.swagger.EnableSwagger2Doc;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@EnableSwagger2Doc // 开启 Swagger
@SpringBootApplication
public class ConfigApplication {
    public static void main(String[] args) {
        SpringApplication.run(ConfigApplication.class, args);
    }
}

执行 ConfigApplication 类启动,在控制台看到成功运行的输出后,打开浏览器访问 localhost:8080/swagger-ui.html 地址,可以看到自动生成的 Swagger API 文档,如图 2-7 所示:

图 2-7 Swagger API 文档

API org.springframework.boot.autoconfigure.EnableAutoConfiguration 注解参数
  • exclude:

Class 数组,排除特定的自动配置类。

  • excludeName:

字符串数组,排除特定名称的自动配置类。

API org.springframework.boot.autoconfigure.ConditionalOnProperty 注解参数
  • havingValue:

字符串,属性期望值是否匹配。

  • matchIfMissing:

布尔值,如果该属性值未设置,则匹配。

  • name:

字符串数组,要测试的属性名。

  • prefix:

字符串,属性前缀名。

  • value:

字符串,功能同 name。

API org.springframework.boot.autoconfigure.ConditionalOnClass 注解参数
  • name:

字符串数组,类名必须存在。

  • value:

Class 数组,类必须存在。

API org.springframework.boot.autoconfigure.ConditionalOnMissingBean 注解参数
  • annotation:

注解 Class 数组,匹配注解装饰的 Bean。

  • ignored:
    Class 数组,匹配时,忽略该类型的 Bean。
  • ignoredType:

字符串数组,匹配时,忽略该类型名称的 Bean。

  • name:

字符串数组,匹配要检查的 Bean 名称。

  • search:

SearchStrategy 对象,通过 SearchStrategy 来决定程序的上下文策略。

  • type:

字符串史胡族,匹配要检查的 Bean 类型名称。

  • value:

Class 数组,匹配要检查的 Bean 类型。

API org.springframework.boot.autoconfigure.ConditionalOnWebApplication 注解参数
  • type:

ConditionalOnWebApplication.Type 对象,匹配对应的 Web 应用程序类型。

2.5 本章小结

本章从自定义属性快速入门工程出发,介绍了两种配置文件以及属性的获取方式,然后讲解了外化配置的优先级、属性引用、随机式使用和多环境配置,最后讲解了自动配置的原理、核心注解以及利用自动配置实现了自定义 Starter 组件。下一章介绍 Spring Boot Web 开发相关。

本章示例代码地址:https://github.com/JeffLi1993...

本文由博客群发一文多发等运营工具平台 OpenWrite 发布

今天关于手写SpringBoot自动配置及自定义注解搭配Aop,实现升级版@Value()功能springboot自己写注解的介绍到此结束,谢谢您的阅读,有关java/springboot 自定义注解实现 AOP、java基础复习-自定义注解3(自定义注解在SpringBoot中的使用)、spring boot aop 打印出入参配置(自定义注解)、Spring Boot 自动配置的原理、核心注解以及利用自动配置实现了自定义 Starter 组件等更多相关知识的信息可以在本站进行查询。

本文标签: