分类: springboot

  • SpringBoot 实现 MySQL 百万级数据量导出并避免 OOM 的解决方案

    快乐分享,Java干货及时送达👇

    动态数据导出是一般项目都会涉及到的功能。它的基本实现逻辑就是从mysql查询数据,加载到内存,然后从内存创建excel或者csv,以流的形式响应给前端。

    • 参考:https://grokonez.com/spring-framework/spring-boot/excel-file-download-from-springboot-restapi-apache-poi-mysql。

    SpringBoot下载excel基本都是这么干。

    虽然这是个可行的方案,然而一旦mysql数据量太大,达到十万级,百万级,千万级,大规模数据加载到内存必然会引起OutofMemoryError

    要考虑如何避免OOM,一般有两个方面的思路。

    一方面就是尽量不做呗,先怼产品下面几个问题啊:

    • 我们为什么要导出这么多数据呢?谁傻到去看这么大的数据啊,这个设计是不是合理的呢?
    • 怎么做好权限控制?百万级数据导出你确定不会泄露商业机密?
    • 如果要导出百万级数据,那为什么不直接找大数据或者DBA来干呢?然后以邮件形式传递不行吗?
    • 为什么要通过后端的逻辑来实现,不考虑时间成本,流量成本吗?
    • 如果通过分页导出,每次点击按钮只导2万条,分批导出难道不能满足业务需求吗?

    如果产品说 “甲方是爸爸,你去和甲方说啊”,“客户说这个做出来,才考虑付尾款!”,如果客户的确缺根筋要让你这样搞, 那就只能从技术上考虑如何实现了。

    从技术上讲,为了避免OOM,我们一定要注意一个原则:

    不能将全量数据一次性加载到内存之中。

    全量加载不可行,那我们的目标就是如何实现数据的分批加载了。实事上,Mysql本身支持Stream查询,我们可以通过Stream流获取数据,然后将数据逐条刷入到文件中,每次刷入文件后再从内存中移除这条数据,从而避免OOM。

    由于采用了数据逐条刷入文件,而且数据量达到百万级,所以文件格式就不要采用excel了,excel2007最大才支持104万行的数据。这里推荐:

    以csv代替excel。

    考虑到当前SpringBoot持久层框架通常为JPA和mybatis,我们可以分别从这两个框架实现百万级数据导出的方案。

    JPA实现百万级数据导出

    • 具体方案不妨参考:http://knes1.github.io/blog/2015/2015-10-19-streaming-mysql-results-using-java8-streams-and-spring-data.html。

    实现项目对应:

    • https://github.com/knes1/todo

    核心注解如下,需要加入到具体的Repository之上。方法的返回类型定义成Stream。Integer.MIN_VALUE告诉jdbc driver逐条返回数据。

    @QueryHints(value = @QueryHint(name = HINT_FETCH_SIZE, value = "" + Integer.MIN_VALUE))
    @Query(value = "select t from Todo t")
    Stream streamAll();

    此外还需要在Stream处理数据的方法之上添加@Transactional(readOnly = true),保证事物是只读的。

    同时需要注入javax.persistence.EntityManager,通过detach从内存中移除已经使用后的对象。

    @RequestMapping(value = "/todos.csv", method = RequestMethod.GET)
    @Transactional(readOnly = true)
    public void exportTodosCSV(HttpServletResponse response) {
     response.addHeader("Content-Type""application/csv");
     response.addHeader("Content-Disposition""attachment; filename=todos.csv");
     response.setCharacterEncoding("UTF-8");
     try(Stream todoStream = todoRepository.streamAll()) {
      PrintWriter out = response.getWriter();
      todoStream.forEach(rethrowConsumer(todo -> {
       String line = todoToCSV(todo);
       out.write(line);
       out.write("n");
       entityManager.detach(todo);
      }));
      out.flush();
     } catch (IOException e) {
      log.info("Exception occurred " + e.getMessage(), e);
      throw new RuntimeException("Exception occurred while exporting results", e);
     }
    }

    MyBatis实现百万级数据导出

    MyBatis实现逐条获取数据,必须要自定义ResultHandler,然后在mapper.xml文件中,对应的select语句中添加fetchSize="-2147483648"

    最后将自定义的ResultHandler传给SqlSession来执行查询,并将返回的结果进行处理。

    MyBatis实现百万级数据导出的具体实例

    以下是基于MyBatis Stream导出的完整的工程样例,我们将通过对比Stream文件导出和传统方式导出的内存占用率的差异,来验证Stream文件导出的有效性。

    我们先定义一个工具类DownloadProcessor,它内部封装一个HttpServletResponse对象,用来将对象写入到csv。

    public class DownloadProcessor {
        private final HttpServletResponse response;
         
        public DownloadProcessor(HttpServletResponse response) {
            this.response = response;
            String fileName = System.currentTimeMillis() + ".csv";
            this.response.addHeader("Content-Type""application/csv");
            this.response.addHeader("Content-Disposition""attachment; filename="+fileName);
            this.response.setCharacterEncoding("UTF-8");
        }
         
        public  void processData(E record) {
            try {
                response.getWriter().write(record.toString()); //如果是要写入csv,需要重写toString,属性通过","分割
                response.getWriter().write("n");
            }catch (IOException e){
                e.printStackTrace();
            }
        }
    }

    然后通过实现org.apache.ibatis.session.ResultHandler,自定义我们的ResultHandler,它用于获取java对象,然后传递给上面的DownloadProcessor处理类进行写文件操作:

    public class CustomResultHandler implements ResultHandler {

        private final DownloadProcessor downloadProcessor;
         
        public CustomResultHandler(
                DownloadProcessor downloadProcessor)
     
    {
            super();
            this.downloadProcessor = downloadProcessor;
        }
         
        @Override
        public void handleResult(ResultContext resultContext) {
            Authors authors = (Authors)resultContext.getResultObject();
            downloadProcessor.processData(authors);
        }
    }

    实体类:

    public class Authors {
        private Integer id;
        private String firstName;
         
        private String lastName;
         
        private String email;
         
        private Date birthdate;
         
        private Date added;
         
        public Integer getId() {
            return id;
        }
         
        public void setId(Integer id) {
            this.id = id;
        }
         
        public String getFirstName() {
            return firstName;
        }
         
        public void setFirstName(String firstName) {
            this.firstName = firstName == null ? null : firstName.trim();
        }
         
        public String getLastName() {
            return lastName;
        }
         
        public void setLastName(String lastName) {
            this.lastName = lastName == null ? null : lastName.trim();
        }
         
        public String getEmail() {
            return email;
        }
         
        public void setEmail(String email) {
            this.email = email == null ? null : email.trim();
        }
         
        public Date getBirthdate() {
            return birthdate;
        }
         
        public void setBirthdate(Date birthdate) {
            this.birthdate = birthdate;
        }
         
        public Date getAdded() {
            return added;
        }
         
        public void setAdded(Date added) {
            this.added = added;
        }
         
        @Override
        public String toString() {
            return this.id + "," + this.firstName + "," + this.lastName + "," + this.email + "," + this.birthdate + "," + this.added;
        }
    }

    Mapper接口:

    public interface AuthorsMapper {
       List selectByExample(AuthorsExample example);
        
       List streamByExample(AuthorsExample example)//以stream形式从mysql获取数据
    }

    Mapper xml文件核心片段,以下两条select的唯一差异就是在stream获取数据的方式中多了一条属性:fetchSize="-2147483648"

    select id="selectByExample" parameterType="com.alphathur.mysqlstreamingexport.domain.AuthorsExample" resultMap="BaseResultMap">
        select
        if test="distinct">
          distinct
        if>
        'false' as QUERYID,
        include refid="Base_Column_List" />
        from authors
        if test="_parameter != null">
          include refid="Example_Where_Clause" />
        if>
        if test="orderByClause != null">
          order by ${orderByClause}
        if>
      select>
      select id="streamByExample" fetchSize="-2147483648" parameterType="com.alphathur.mysqlstreamingexport.domain.AuthorsExample" resultMap="BaseResultMap">
        select
        if test="distinct">
          distinct
        if>
        'false' as QUERYID,
        include refid="Base_Column_List" />
        from authors
        if test="_parameter != null">
          include refid="Example_Where_Clause" />
        if>
        if test="orderByClause != null">
          order by ${orderByClause}
        if>
      select>

    获取数据的核心service如下,由于只做个简单演示,就懒得写成接口了。其中 streamDownload 方法即为stream取数据写文件的实现,它将以很低的内存占用从MySQL获取数据;此外还提供traditionDownload方法,它是一种传统的下载方式,批量获取全部数据,然后将每个对象写入文件。

    @Service
    public class AuthorsService {
        private final SqlSessionTemplate sqlSessionTemplate;
        private final AuthorsMapper authorsMapper;

        public AuthorsService(SqlSessionTemplate sqlSessionTemplate, AuthorsMapper authorsMapper) {
            this.sqlSessionTemplate = sqlSessionTemplate;
            this.authorsMapper = authorsMapper;
        }

        /**
         * stream读数据写文件方式
         * @param httpServletResponse
         * @throws IOException
         */

        public void streamDownload(HttpServletResponse httpServletResponse)
                throws IOException 
    {
            AuthorsExample authorsExample = new AuthorsExample();
            authorsExample.createCriteria();
            HashMap param = new HashMap();
            param.put("oredCriteria", authorsExample.getOredCriteria());
            param.put("orderByClause", authorsExample.getOrderByClause());
            CustomResultHandler customResultHandler = new CustomResultHandler(new DownloadProcessor (httpServletResponse));
            sqlSessionTemplate.select(
                    "com.alphathur.mysqlstreamingexport.mapper.AuthorsMapper.streamByExample", param, customResultHandler);
            httpServletResponse.getWriter().flush();
            httpServletResponse.getWriter().close();
        }

        /**
         * 传统下载方式
         * @param httpServletResponse
         * @throws IOException
         */

        public void traditionDownload(HttpServletResponse httpServletResponse)
                throws IOException 
    {
            AuthorsExample authorsExample = new AuthorsExample();
            authorsExample.createCriteria();
            List authors = authorsMapper.selectByExample (authorsExample);
            DownloadProcessor downloadProcessor = new DownloadProcessor (httpServletResponse);
            authors.forEach (downloadProcessor::processData);
            httpServletResponse.getWriter().flush();
            httpServletResponse.getWriter().close();
        }
    }

    下载的入口controller:

    @RestController
    @RequestMapping("download")
    public class HelloController {
        private final AuthorsService authorsService;

        public HelloController(AuthorsService authorsService) {
            this.authorsService = authorsService;
        }

        @GetMapping("streamDownload")
        public void streamDownload(HttpServletResponse response)
                throws IOException 
    {
            authorsService.streamDownload(response);
        }

        @GetMapping("traditionDownload")
        public void traditionDownload(HttpServletResponse response)
                throws IOException 
    {
            authorsService.traditionDownload (response);
        }
    }   

    实体类对应的表结构创建语句:

    CREATE TABLE `authors` (
      `id` int(11NOT NULL AUTO_INCREMENT,
      `first_name` varchar(50CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL,
      `last_name` varchar(50CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL,
      `email` varchar(100CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL,
      `birthdate` date NOT NULL,
      `added` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
      PRIMARY KEY (`id`)
    ENGINE=InnoDB AUTO_INCREMENT=10095 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

    这里有个问题:如何短时间内创建大批量测试数据到MySQL呢?一种方式是使用存储过程 + 大杀器 select insert 语句!不太懂?

    没关系,且看我另一篇文章 MySQL如何生成大批量测试数据 你就会明白了。如果你懒得看,我这里已经将生成的270多万条测试数据上传到网盘,你直接下载然后通过navicat导入就好了。

    • 链接:https://pan.baidu.com/s/1hqnWU2JKlL4Tb9nWtJl4sw
    • 提取码:nrp0

    有了测试数据,我们就可以直接测试了。先启动项目,然后打开jdk bin目录下的 jconsole.exe

    首先我们测试传统方式下载文件的内存占用,直接浏览器访问:http://localhost:8080/download/traditionDownload

    可以看出,下载开始前内存占用大概为几十M,下载开始后内存占用急速上升,峰值达到接近2.5G,即使是下载完成,堆内存也维持一个较高的占用,这实在是太可怕了,如果生产环境敢这么搞,不出意外肯定内存溢出。

    接着我们测试stream方式文件下载的内存占用,浏览器访问:http://localhost:8080/download/streamDownload,当下载开始后,内存占用也会有一个明显的上升,但是峰值才到500M。对比于上面的方式,内存占用率足足降低了80%!怎么样,兴奋了吗!

    我们再通过记事本打开下载后的两个文件,发现内容没有缺斤少两,都是2727127行,完美!

    感谢阅读,希望对你有所帮助 🙂 来源:

    blog.csdn.net/haohao_ding/article/details/123164771

  • SpringBoot整合Canal+RabbitMQ监听数据变更~

    快乐分享,Java干货及时送达👇

    来源:JAVA日知录

    • 需求
    • 步骤
    • 环境搭建
    • 整合SpringBoot Canal实现客户端
    • Canal整合RabbitMQ
    • SpringBoot整合RabbitMQ

    需求

    我想要在SpringBoot中采用一种与业务代码解耦合的方式,来实现数据的变更记录,记录的内容是新数据,如果是更新操作还得有旧数据内容。

    经过调研发现,使用Canal来监听MySQL的binlog变化可以实现这个需求,可是在监听到变化后需要马上保存变更记录,除非再做一些逻辑处理,于是我又结合了RabbitMQ来处理保存变更记录的操作。

    步骤

    • 启动MySQL环境,并开启binlog
    • 启动Canal环境,为其创建一个MySQL账号,然后以Slave的形式连接MySQL
    • Canal服务模式设为TCP,用Java编写客户端代码,监听MySQL的binlog修改
    • Canal服务模式设为RabbitMQ,启动RabbitMQ环境,配置Canal和RabbitMQ的连接,用消息队列去接收binlog修改事件

    环境搭建

    环境搭建基于docker-compose:

    version: "3"  
    services:  
        mysql:  
            network_mode: mynetwork  
            container_name: mymysql  
            ports:  
                - 3306:3306  
            restart: always  
            volumes:  
                - /etc/localtime:/etc/localtime  
                - /home/mycontainers/mymysql/data:/data  
                - /home/mycontainers/mymysql/mysql:/var/lib/mysql  
                - /home/mycontainers/mymysql/conf:/etc/mysql  
            environment:  
                - MYSQL_ROOT_PASSWORD=root  
            command:   
                --character-set-server=utf8mb4  
                --collation-server=utf8mb4_unicode_ci  
                --log-bin=/var/lib/mysql/mysql-bin  
                --server-id=1  
                --binlog-format=ROW  
                --expire_logs_days=7  
                --max_binlog_size=500M  
            image: mysql:5.7.20  
        rabbitmq:     
            container_name: myrabbit  
            ports:  
                - 15672:15672  
                - 5672:5672  
            restart: always  
            volumes:  
                - /etc/localtime:/etc/localtime  
                - /home/mycontainers/myrabbit/rabbitmq:/var/lib/rabbitmq  
            network_mode: mynetwork  
            environment:  
                - RABBITMQ_DEFAULT_USER=admin  
                - RABBITMQ_DEFAULT_PASS=123456  
            image: rabbitmq:3.8-management  
        canal-server:  
            container_name: canal-server  
            restart: always  
            ports:  
                - 11110:11110  
                - 11111:11111  
                - 11112:11112  
            volumes:  
                - /home/mycontainers/canal-server/conf/canal.properties:/home/admin/canal-server/conf/canal.properties  
                - /home/mycontainers/canal-server/conf/instance.properties:/home/admin/canal-server/conf/example/instance.properties  
                - /home/mycontainers/canal-server/logs:/home/admin/canal-server/logs  
            network_mode: mynetwork  
            depends_on:  
                - mysql  
                - rabbitmq  
                # - canal-admin  
            image: canal/canal-server:v1.1.5  

    我们需要修改下Canal环境的配置文件:canal.propertiesinstance.properties,映射Canal中的以下两个路径:

    • /home/admin/canal-server/conf/canal.properties

    配置文件中,canal.destinations意思是server上部署的instance列表,

    • /home/admin/canal-server/conf/example/instance.properties

    这里的/example是指instance即实例名,要和上面canal.properties内instance配置对应,canal会为实例创建对应的文件夹,一个Client对应一个实例

    以下是我们需要准备的两个配置文件具体内容:

    canal.properties

    ################################################  
    ########     common argument   ############  
    ################################################  
    # tcp bind ip  
    canal.ip =  
    # register ip to zookeeper  
    canal.register.ip =  
    canal.port = 11111  
    canal.metrics.pull.port = 11112  
    # canal instance user/passwd  
    # canal.user = canal  
    # canal.passwd = E3619321C1A937C46A0D8BD1DAC39F93B27D4458  
      
    # canal admin config  
    # canal.admin.manager = canal-admin:8089  
      
    # canal.admin.port = 11110  
    # canal.admin.user = admin  
    # canal.admin.passwd = 6BB4837EB74329105EE4568DDA7DC67ED2CA2AD9  
      
    # admin auto register 自动注册  
    # canal.admin.register.auto = true  
    # 集群名,单机则不写  
    # canal.admin.register.cluster =  
    # Canal Server 名字  
    # canal.admin.register.name = canal-admin  
      
    canal.zkServers =  
    # flush data to zk  
    canal.zookeeper.flush.period = 1000  
    canal.withoutNetty = false  
    # tcp, kafka, rocketMQ, rabbitMQ, pulsarMQ  
    canal.serverMode = tcp  
    # flush meta cursor/parse position to file  
    canal.file.data.dir = ${canal.conf.dir}  
    canal.file.flush.period = 1000  
    # memory store RingBuffer size, should be Math.pow(2,n)  
    canal.instance.memory.buffer.size = 16384  
    # memory store RingBuffer used memory unit size , default 1kb  
    canal.instance.memory.buffer.memunit = 1024   
    # meory store gets mode used MEMSIZE or ITEMSIZE  
    canal.instance.memory.batch.mode = MEMSIZE  
    canal.instance.memory.rawEntry = true  
      
    # detecing config  
    canal.instance.detecting.enable = false  
    #canal.instance.detecting.sql = insert into retl.xdual values(1,now()) on duplicate key update x=now()  
    canal.instance.detecting.sql = select 1  
    canal.instance.detecting.interval.time = 3  
    canal.instance.detecting.retry.threshold = 3  
    canal.instance.detecting.heartbeatHaEnable = false  
      
    # support maximum transaction size, more than the size of the transaction will be cut into multiple transactions delivery  
    canal.instance.transaction.size =  1024  
    # mysql fallback connected to new master should fallback times  
    canal.instance.fallbackIntervalInSeconds = 60  
      
    # network config  
    canal.instance.network.receiveBufferSize = 16384  
    canal.instance.network.sendBufferSize = 16384  
    canal.instance.network.soTimeout = 30  
      
    # binlog filter config  
    canal.instance.filter.druid.ddl = true  
    canal.instance.filter.query.dcl = false  
    canal.instance.filter.query.dml = false  
    canal.instance.filter.query.ddl = false  
    canal.instance.filter.table.error = false  
    canal.instance.filter.rows = false  
    canal.instance.filter.transaction.entry = false  
    canal.instance.filter.dml.insert = false  
    canal.instance.filter.dml.update = false  
    canal.instance.filter.dml.delete = false  
      
    # binlog format/image check  
    canal.instance.binlog.format = ROW,STATEMENT,MIXED   
    canal.instance.binlog.image = FULL,MINIMAL,NOBLOB  
      
    # binlog ddl isolation  
    canal.instance.get.ddl.isolation = false  
      
    # parallel parser config  
    canal.instance.parser.parallel = true  
    # concurrent thread number, default 60% available processors, suggest not to exceed Runtime.getRuntime().availableProcessors()  
    canal.instance.parser.parallelThreadSize = 16  
    # disruptor ringbuffer size, must be power of 2  
    canal.instance.parser.parallelBufferSize = 256  
      
    # table meta tsdb info  
    canal.instance.tsdb.enable = true  
    canal.instance.tsdb.dir = ${canal.file.data.dir:../conf}/${canal.instance.destination:}  
    canal.instance.tsdb.url = jdbc:h2:${canal.instance.tsdb.dir}/h2;CACHE_SIZE=1000;MODE=MYSQL;  
    canal.instance.tsdb.dbUsername = canal  
    canal.instance.tsdb.dbPassword = canal  
    # dump snapshot interval, default 24 hour  
    canal.instance.tsdb.snapshot.interval = 24  
    # purge snapshot expire , default 360 hour(15 days)  
    canal.instance.tsdb.snapshot.expire = 360  
      
    ################################################  
    ########     destinations    ############  
    ################################################  
    canal.destinations = canal-exchange  
    # conf root dir  
    canal.conf.dir = ../conf  
    # auto scan instance dir add/remove and start/stop instance  
    canal.auto.scan = true  
    canal.auto.scan.interval = 5  
    # set this value to 'true' means that when binlog pos not found, skip to latest.  
    # WARN: pls keep 'false' in production env, or if you know what you want.  
    canal.auto.reset.latest.pos.mode = false  
      
    canal.instance.tsdb.spring.xml = classpath:spring/tsdb/h2-tsdb.xml  
    #canal.instance.tsdb.spring.xml = classpath:spring/tsdb/mysql-tsdb.xml  
      
    canal.instance.global.mode = spring  
    canal.instance.global.lazy = false  
    canal.instance.global.manager.address = ${canal.admin.manager}  
    #canal.instance.global.spring.xml = classpath:spring/memory-instance.xml  
    canal.instance.global.spring.xml = classpath:spring/file-instance.xml  
    #canal.instance.global.spring.xml = classpath:spring/default-instance.xml  
      
    #################################################  
    ########         MQ Properties      ############  
    #################################################  
    # aliyun ak/sk , support rds/mq  
    canal.aliyun.accessKey =  
    canal.aliyun.secretKey =  
    canal.aliyun.uid=  
      
    canal.mq.flatMessage = true  
    canal.mq.canalBatchSize = 50  
    canal.mq.canalGetTimeout = 100  
    # Set this value to "cloud", if you want open message trace feature in aliyun.  
    canal.mq.accessChannel = local  
      
    canal.mq.database.hash = true  
    canal.mq.send.thread.size = 30  
    canal.mq.build.thread.size = 8  
      
    #################################################  
    ########         RabbitMQ       ############  
    #################################################  
    rabbitmq.host = myrabbit  
    rabbitmq.virtual.host = /  
    rabbitmq.exchange = canal-exchange  
    rabbitmq.username = admin  
    rabbitmq.password = RabbitMQ密码  
    rabbitmq.deliveryMode =  

    此时canal.serverMode = tcp,即TCP直连,我们先开启这个服务,然后手写Java客户端代码去连接它,等下再改为RabbitMQ。

    通过注释可以看到,canal支持的服务模式有:tcp, kafka, rocketMQ, rabbitMQ, pulsarMQ,即主流的消息队列都支持。

    instance.properties

    ################################################  
    # mysql serverId , v1.0.26+ will autoGen  
    #canal.instance.mysql.slaveId=123  
      
    # enable gtid use true/false  
    canal.instance.gtidon=false  
      
    # position info  
    canal.instance.master.address=mymysql:3306  
    canal.instance.master.journal.name=  
    canal.instance.master.position=  
    canal.instance.master.timestamp=  
    canal.instance.master.gtid=  
      
    # rds oss binlog  
    canal.instance.rds.accesskey=  
    canal.instance.rds.secretkey=  
    canal.instance.rds.instanceId=  
      
    # table meta tsdb info  
    canal.instance.tsdb.enable=true  
    #canal.instance.tsdb.url=jdbc:mysql://127.0.0.1:3306/canal_tsdb  
    #canal.instance.tsdb.dbUsername=canal  
    #canal.instance.tsdb.dbPassword=canal  
      
    #canal.instance.standby.address =  
    #canal.instance.standby.journal.name =  
    #canal.instance.standby.position =  
    #canal.instance.standby.timestamp =  
    #canal.instance.standby.gtid=  
      
    # username/password  
    canal.instance.dbUsername=canal  
    canal.instance.dbPassword=canal  
    canal.instance.connectionCharset = UTF-8  
    # enable druid Decrypt database password  
    canal.instance.enableDruid=false  
    #canal.instance.pwdPublicKey=MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBALK4BUxdDltRRE5/zXpVEVPUgunvscYFtEip3pmLlhrWpacX7y7GCMo2/JM6LeHmiiNdH1FWgGCpUfircSwlWKUCAwEAAQ==  
      
    # table regex  
    canal.instance.filter.regex=.*..*  
    # table black regex  
    canal.instance.filter.black.regex=mysql.slave_.*  
    # table field filter(format: schema1.tableName1:field1/field2,schema2.tableName2:field1/field2)  
    #canal.instance.filter.field=test1.t_product:id/subject/keywords,test2.t_company:id/name/contact/ch  
    # table field black filter(format: schema1.tableName1:field1/field2,schema2.tableName2:field1/field2)  
    #canal.instance.filter.black.field=test1.t_product:subject/product_image,test2.t_company:id/name/contact/ch  
      
    # mq config  
    canal.mq.topic=canal-routing-key  
    # dynamic topic route by schema or table regex  
    #canal.mq.dynamicTopic=mytest1.user,topic2:mytest2..*,.*..*  
    canal.mq.partition=0  

    把这两个配置文件映射好,再次提醒,注意实例的路径名,默认是:/example/instance.properties

    修改canal配置文件

    我们需要修改这个实例配置文件,去连接MySQL,确保以下的配置正确:

    canal.instance.master.address=mymysql:3306  
    canal.instance.dbUsername=canal  
    canal.instance.dbPassword=canal  

    mymysql是同为docker容器的MySQL环境,端口3306是指内部端口。

    这里多说明一下,docker端口配置时假设为:13306:3306,那么容器对外的端口就是13306,内部是3306,在本示例中,MySQL和Canal都是容器环境,所以Canal连接MySQL需要满足以下条件:

    • 处于同一网段(docker-compose.yml中的mynetwork)
    • 访问内部端口(即3306,而非13306)

    dbUsername和dbPassword为MySQL账号密码,为了开发方便可以使用root/root,但是我仍建议自行创建用户并分配访问权限:

    # 进入docker中的mysql容器  
    docker exec -it mymysql bash  
    # 进入mysql指令模式  
    mysql -uroot -proot  
      
    # 编写MySQL语句并执行  
    > ...  
    -- 选择mysql  
    use mysql;  
    -- 创建canal用户,账密:canal/canal  
    create user 'canal'@'%' identified by 'canal';  
    -- 分配权限,以及允许所有主机登录该用户  
    grant SELECT, INSERT, UPDATE, DELETE, REPLICATION SLAVE, REPLICATION CLIENT on *.* to 'canal'@'%';  
      
    -- 刷新一下使其生效  
    flush privileges;  
      
    -- 附带一个删除用户指令  
    drop user 'canal'@'%';  

    用navicat或者shell去登录canal这个用户,可以访问即创建成功

    整合SpringBoot Canal实现客户端

    Maven依赖:

    1.1.5  
      
      
      
      com.alibaba.otter  
      canal.client  
      ${canal.version}  
      
      
      com.alibaba.otter  
      canal.protocol  
      ${canal.version}  
       

    新增组件并启动:

    import com.alibaba.otter.canal.client.CanalConnector;  
    import com.alibaba.otter.canal.client.CanalConnectors;  
    import com.alibaba.otter.canal.protocol.CanalEntry;  
    import com.alibaba.otter.canal.protocol.Message;  
    import org.springframework.boot.CommandLineRunner;  
    import org.springframework.stereotype.Component;  
      
    import java.net.InetSocketAddress;  
    import java.util.List;  
      
    @Component  
    public class CanalClient {  
      
        private final static int BATCH_SIZE = 1000;  
      
        public void run() {  
            // 创建链接  
            CanalConnector connector = CanalConnectors.newSingleConnector(new InetSocketAddress("localhost", 11111), "canal-exchange""canal""canal");  
            try {  
                //打开连接  
                connector.connect();  
                //订阅数据库表,全部表  
                connector.subscribe(".*..*");  
                //回滚到未进行ack的地方,下次fetch的时候,可以从最后一个没有ack的地方开始拿  
                connector.rollback();  
                while (true) {  
                    // 获取指定数量的数据  
                    Message message = connector.getWithoutAck(BATCH_SIZE);  
                    //获取批量ID  
                    long batchId = message.getId();  
                    //获取批量的数量  
                    int size = message.getEntries().size();  
                    //如果没有数据  
                    if (batchId == -1 || size == 0) {  
                        try {  
                            //线程休眠2秒  
                            Thread.sleep(2000);  
                        } catch (InterruptedException e) {  
                            e.printStackTrace();  
                        }  
                    } else {  
                        //如果有数据,处理数据  
                        printEntry(message.getEntries());  
                    }  
                    //进行 batch id 的确认。确认之后,小于等于此 batchId 的 Message 都会被确认。  
                    connector.ack(batchId);  
                }  
            } catch (Exception e) {  
                e.printStackTrace();  
            } finally {  
                connector.disconnect();  
            }  
        }  
      
        /**  
         * 打印canal server解析binlog获得的实体类信息  
         */  
        private static void printEntry(List entrys) {  
            for (CanalEntry.Entry entry : entrys) {  
                if (entry.getEntryType() == CanalEntry.EntryType.TRANSACTIONBEGIN || entry.getEntryType() == CanalEntry.EntryType.TRANSACTIONEND) {  
                    //开启/关闭事务的实体类型,跳过  
                    continue;  
                }  
                //RowChange对象,包含了一行数据变化的所有特征  
                //比如isDdl 是否是ddl变更操作 sql 具体的ddl sql beforeColumns afterColumns 变更前后的数据字段等等  
                CanalEntry.RowChange rowChage;  
                try {  
                    rowChage = CanalEntry.RowChange.parseFrom(entry.getStoreValue());  
                } catch (Exception e) {  
                    throw new RuntimeException("ERROR # parser of eromanga-event has an error , data:" + entry.toString(), e);  
                }  
                //获取操作类型:insert/update/delete类型  
                CanalEntry.EventType eventType = rowChage.getEventType();  
                //打印Header信息  
                System.out.println(String.format("================》; binlog[%s:%s] , name[%s,%s] , eventType : %s",  
                        entry.getHeader().getLogfileName(), entry.getHeader().getLogfileOffset(),  
                        entry.getHeader().getSchemaName(), entry.getHeader().getTableName(),  
                        eventType));  
                //判断是否是DDL语句  
                if (rowChage.getIsDdl()) {  
                    System.out.println("================》;isDdl: true,sql:" + rowChage.getSql());  
                }  
                //获取RowChange对象里的每一行数据,打印出来  
                for (CanalEntry.RowData rowData : rowChage.getRowDatasList()) {  
                    //如果是删除语句  
                    if (eventType == CanalEntry.EventType.DELETE) {  
                        printColumn(rowData.getBeforeColumnsList());  
                        //如果是新增语句  
                    } else if (eventType == CanalEntry.EventType.INSERT) {  
                        printColumn(rowData.getAfterColumnsList());  
                        //如果是更新的语句  
                    } else {  
                        //变更前的数据  
                        System.out.println("------->; before");  
                        printColumn(rowData.getBeforeColumnsList());  
                        //变更后的数据  
                        System.out.println("------->; after");  
                        printColumn(rowData.getAfterColumnsList());  
                    }  
                }  
            }  
        }  
      
        private static void printColumn(List columns) {  
            for (CanalEntry.Column column : columns) {  
                System.out.println(column.getName() + " : " + column.getValue() + "    update=" + column.getUpdated());  
            }  
        }  
    }  

    启动类Application:

    @SpringBootApplication  
    public class BaseApplication implements CommandLineRunner {  
        @Autowired  
        private CanalClient canalClient;  
      
        @Override  
        public void run(String... args) throws Exception {  
            canalClient.run();  
        }  
    }  

    启动程序,此时新增或修改数据库中的数据,我们就能从客户端中监听到

    不过我建议监听的信息放到消息队列中,在空闲的时候去处理,所以直接配置Canal整合RabbitMQ更好。

    Canal整合RabbitMQ

    修改canal.properties中的serverMode:

    canal.serverMode = rabbitMQ  

    修改instance.properties中的topic:

    canal.mq.topic=canal-routing-key  

    然后找到关于RabbitMQ的配置:

    #################################################  
    ########         RabbitMQ       ############  
    #################################################  
    # 连接rabbit,写IP,因为同个网络下,所以可以写容器名  
    rabbitmq.host = myrabbit  
    rabbitmq.virtual.host = /  
    # 交换器名称,等等我们要去手动创建  
    rabbitmq.exchange = canal-exchange  
    # 账密  
    rabbitmq.username = admin  
    rabbitmq.password = 123456  
    # 暂不支持指定端口,使用的是默认的5762,好在在本示例中适用  

    重新启动容器,进入RabbitMQ管理页面创建exchange交换器和队列queue:

    • 新建exchange,命名为:canal-exchange
    • 新建queue,命名为:canal-queue
    • 绑定exchange和queue,routing-key设置为:canal-routing-key,这里对应上面instance.propertiescanal.mq.topic

    顺带一提,上面这段可以忽略,因为在SpringBoot的RabbitMQ配置中,会自动创建交换器exchange和队列queue,不过手动创建的话,可以在忽略SpringBoot的基础上,直接在RabbitMQ的管理页面上看到修改记录的消息。

    SpringBoot整合RabbitMQ

    依赖:

    2.3.4.RELEASE  
      
      
      
      org.springframework.boot  
      spring-boot-starter-amqp  
      ${amqp.version}  
      

    application.yml

    spring:  
      rabbitmq:  
        #    host: myserverhost  
        host: 192.168.0.108  
        port: 5672  
        username: admin  
        password: RabbitMQ密码  
        # 消息确认配置项  
        # 确认消息已发送到交换机(Exchange)  
        publisher-confirm-type: correlated  
        # 确认消息已发送到队列(Queue)  
        publisher-returns: true  

    RabbitMQ配置类:

    @Configuration  
    public class RabbitConfig {  
        @Bean  
        public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {  
            RabbitTemplate template = new RabbitTemplate();  
            template.setConnectionFactory(connectionFactory);  
            template.setMessageConverter(new Jackson2JsonMessageConverter());  
      
            return template;  
        }  
      
        /**  
         * template.setMessageConverter(new Jackson2JsonMessageConverter());  
         * 这段和上面这行代码解决RabbitListener循环报错的问题  
         */  
        @Bean  
        public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(ConnectionFactory connectionFactory) {  
            SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();  
            factory.setConnectionFactory(connectionFactory);  
            factory.setMessageConverter(new Jackson2JsonMessageConverter());  
            return factory;  
        }  
    }  

    Canal消息生产者:

    public static final String CanalQueue = "canal-queue";  
    public static final String CanalExchange = "canal-exchange";  
    public static final String CanalRouting = "canal-routing-key";  

    /**  
     * Canal消息提供者,canal-server生产的消息通过RabbitMQ消息队列发送  
     */  
    @Configuration  
    public class CanalProvider {  
        /**  
         * 队列  
         */  
        @Bean  
        public Queue canalQueue() {  
            /**  
             * durable:是否持久化,默认false,持久化队列:会被存储在磁盘上,当消息代理重启时仍然存在;暂存队列:当前连接有效  
             * exclusive:默认为false,只能被当前创建的连接使用,而且当连接关闭后队列即被删除。此参考优先级高于durable  
             * autoDelete:是否自动删除,当没有生产者或者消费者使用此队列,该队列会自动删除  
             */  
            return new Queue(RabbitConstant.CanalQueue, true);  
        }  
      
        /**  
         * 交换机,这里使用直连交换机  
         */  
        @Bean  
        DirectExchange canalExchange() {  
            return new DirectExchange(RabbitConstant.CanalExchange, truefalse);  
        }  
      
        /**  
         * 绑定交换机和队列,并设置匹配键  
         */  
        @Bean  
        Binding bindingCanal() {  
            return BindingBuilder.bind(canalQueue()).to(canalExchange()).with(RabbitConstant.CanalRouting);  
        }  
    }  

    Canal消息消费者:

    /**  
     * Canal消息消费者  
     */  
    @Component  
    @RabbitListener(queues = RabbitConstant.CanalQueue)  
    public class CanalComsumer {  
        private final SysBackupService sysBackupService;  
      
        public CanalComsumer(SysBackupService sysBackupService) {  
            this.sysBackupService = sysBackupService;  
        }  
      
        @RabbitHandler  
        public void process(Map msg) {  
            System.out.println("收到canal消息:" + msg);  
            boolean isDdl = (boolean) msg.get("isDdl");  
      
            // 不处理DDL事件  
            if (isDdl) {  
                return;  
            }  
      
            // TiCDC的id,应该具有唯一性,先保存再说  
            int tid = (int) msg.get("id");  
            // TiCDC生成该消息的时间戳,13位毫秒级  
            long ts = (long) msg.get("ts");  
            // 数据库  
            String database = (String) msg.get("database");  
            // 表  
            String table = (String) msg.get("table");  
            // 类型:INSERT/UPDATE/DELETE  
            String type = (String) msg.get("type");  
            // 每一列的数据值  
            List> data = (List>) msg.get("data");  
            // 仅当type为UPDATE时才有值,记录每一列的名字和UPDATE之前的数据值  
            List> old = (List>) msg.get("old");  
      
            // 跳过sys_backup,防止无限循环  
            if ("sys_backup".equalsIgnoreCase(table)) {  
                return;  
            }  
      
            // 只处理指定类型  
            if (!"INSERT".equalsIgnoreCase(type)  
                    && !"UPDATE".equalsIgnoreCase(type)  
                    && !"DELETE".equalsIgnoreCase(type)) {  
                return;  
            }  
        }  
    }  

    测试一下,修改MySQL中的一条消息,Canal就会发送信息到RabbitMQ,我们就能从监听的RabbitMQ队列中得到该条消息。

  • Spring Cloud 的25连环炮!

    快乐分享,Java干货及时送达👇

    来源:Java后端面试官

    • 前言
    • Spring Cloud核心知识总结
    • 连环炮走起
    • 总结

    前言

    上周,一位朋友在面试被问到了Spring Cloud,然后结合他的反馈,今天我们继续走起SpringCloud面试连环炮。

    Spring Cloud核心知识总结

    下面是一张Spring Cloud核心组件关系图:

    图片

    从这张图中,其实我们是可以获取很多信息的,希望大家细细品尝。

    话不多说,我们直接开始 Spring Cloud 连环炮。

    连环炮走起

    1、什么是Spring Cloud ?

    Spring cloud 流应用程序启动器是基于 Spring Boot 的 Spring 集成应用程序,提供与外部系统的集成。Spring cloud Task,一个生命周期短暂的微服务框架,用于快速构建执行有限数据处理的应用程序。

    2、什么是微服务?

    微服务架构是一种架构模式或者说是一种架构风格,它提倡将单一应用程序划分为一组小的服务,每个服务运行在其独立的自己的进程中,服务之间相互协调、互相配合,为用户提供最终价值。服务之间采用轻量级的通信机制互相沟通(通常是基于HTTP的RESTful API),每个服务都围绕着具体的业务进行构建,并且能够被独立的构建在生产环境、类生产环境等。另外,应避免统一的、集中式的服务管理机制,对具体的一个服务而言,应根据业务上下文,选择合适的语言、工具对其进行构建,可以有一个非常轻量级的集中式管理来协调这些服务,可以使用不同的语言来编写服务,也可以使用不同的数据存储。

    通俗地来讲:

    微服务就是一个独立的职责单一的服务应用程序。在 intellij idea 工具里面就是用maven开发的一个个独立的module,具体就是使用springboot 开发的一个小的模块,处理单一专业的业务逻辑,一个模块只做一个事情。

    微服务强调的是服务大小,关注的是某一个点,具体解决某一个问题/落地对应的一个服务应用,可以看做是idea 里面一个 module。

    3、Spring Cloud有什么优势

    使用 Spring Boot 开发分布式微服务时,我们面临以下问题

    • 与分布式系统相关的复杂性-这种开销包括网络问题,延迟开销,带宽问题,安全问题。
    • 服务发现-服务发现工具管理群集中的流程和服务如何查找和互相交谈。它涉及一个服务目录,在该目录中注册服务,然后能够查找并连接到该目录中的服务。
    • 冗余-分布式系统中的冗余问题。
    • 负载平衡 –负载平衡改善跨多个计算资源的工作负荷,诸如计算机,计算机集群,网络链路,中央处理单元,或磁盘驱动器的分布。
    • 性能-问题 由于各种运营开销导致的性能问题。
    • 部署复杂性-Devops 技能的要求。

    4、微服务之间如何独立通讯的?

    同步通信:dobbo通过 RPC 远程过程调用、springcloud通过 REST  接口json调用等。

    异步:消息队列,如:RabbitMqActiveMKafka等消息队列。

    5、什么是服务熔断?什么是服务降级?

    熔断机制是应对雪崩效应的一种微服务链路保护机制。当某个微服务不可用或者响应时间太长时,会进行服务降级,进而熔断该节点微服务的调用,快速返回“错误”的响应信息。当检测到该节点微服务调用响应正常后恢复调用链路。在Spring Cloud框架里熔断机制通过Hystrix实现,Hystrix会监控微服务间调用的状况,当失败的调用到一定阈值,缺省是5秒内调用20次,如果失败,就会启动熔断机制。

    服务降级,一般是从整体负荷考虑。就是当某个服务熔断之后,服务器将不再被调用,此时客户端可以自己准备一个本地的fallback回调,返回一个缺省值。这样做,虽然水平下降,但好歹可用,比直接挂掉强。

    Hystrix相关注解@EnableHystrix:开启熔断 @HystrixCommand(fallbackMethod=”XXX”),声明一个失败回滚处理函数XXX,当被注解的方法执行超时(默认是1000毫秒),就会执行fallback函数,返回错误提示。

    6、请说说Eureka和zookeeper 的区别?

    Zookeeper保证了CP,Eureka保证了AP。

    A:高可用

    C:一致性

    P:分区容错性

    1.当向注册中心查询服务列表时,我们可以容忍注册中心返回的是几分钟以前的信息,但不能容忍直接down掉不可用。也就是说,服务注册功能对高可用性要求比较高,但zk会出现这样一种情况,当master节点因为网络故障与其他节点失去联系时,剩余节点会重新选leader。问题在于,选取leader时间过长,30 ~ 120s,且选取期间zk集群都不可用,这样就会导致选取期间注册服务瘫痪。在云部署的环境下,因网络问题使得zk集群失去master节点是较大概率会发生的事,虽然服务能够恢复,但是漫长的选取时间导致的注册长期不可用是不能容忍的。

    2.Eureka保证了可用性,Eureka各个节点是平等的,几个节点挂掉不会影响正常节点的工作,剩余的节点仍然可以提供注册和查询服务。而Eureka的客户端向某个Eureka注册或发现时发生连接失败,则会自动切换到其他节点,只要有一台Eureka还在,就能保证注册服务可用,只是查到的信息可能不是最新的。除此之外,Eureka还有自我保护机制,如果在15分钟内超过85%的节点没有正常的心跳,那么Eureka就认为客户端与注册中心发生了网络故障,此时会出现以下几种情况:

    ①、Eureka不在从注册列表中移除因为长时间没有收到心跳而应该过期的服务。

    ②、Eureka仍然能够接受新服务的注册和查询请求,但是不会被同步到其他节点上(即保证当前节点仍然可用)

    ③、当网络稳定时,当前实例新的注册信息会被同步到其他节点。

    因此,Eureka可以很好地应对因网络故障导致部分节点失去联系的情况,而不会像Zookeeper那样使整个微服务瘫痪

    7、SpringBoot和SpringCloud的区别?

    SpringBoot专注于快速方便得开发单个个体微服务。

    SpringCloud是关注全局的微服务协调整理治理框架,它将SpringBoot开发的一个个单体微服务整合并管理起来,

    为各个微服务之间提供,配置管理、服务发现、断路器、路由、微代理、事件总线、全局锁、决策竞选、分布式会话等等集成服务

    SpringBoot可以离开SpringCloud独立使用开发项目, 但是SpringCloud离不开SpringBoot ,属于依赖的关系.

    SpringBoot专注于快速、方便得开发单个微服务个体,SpringCloud关注全局的服务治理框架。

    8、负载平衡的意义什么?

    在计算中,负载平衡可以改善跨计算机,计算机集群,网络链接,中央处理单元或磁盘驱动器等多种计算资源的工作负载分布。负载平衡旨在优化资源使用,最大化吞吐量,最小化响应时间并避免任何单一资源 的过载。使用多个组件进行负载平衡而不是单个组件可能会通过冗余来提高可靠性和可用性。负载平衡通常涉及专用软件或硬件,例如多层交换机或域名系统服务器进程。

    9、什么是Hystrix?它如何实现容错?

    Hystrix是一个延迟和容错库,旨在隔离远程系统,服务和第三方库的访问点,当出现故障是不可避免的故障时,停止级联故障并在复杂的分布式系统中实现弹性。

    通常对于使用微服务架构开发的系统,涉及到许多微服务。这些微服务彼此协作。

    思考一下微服务:

    图片

    假设如果上图中的微服务9失败了,那么使用传统方法我们将传播一个异常。但这仍然会导致整个系统崩溃。

    随着微服务数量的增加,这个问题变得更加复杂。微服务的数量可以高达1000.这是hystrix出现的地方 我们将使用Hystrix在这种情况下的Fallback方法功能。我们有两个服务employee-consumer使用由employee-consumer公开的服务。

    简化图如下所示

    图片

    现在假设由于某种原因,employee-producer公开的服务会抛出异常。我们在这种情况下使用Hystrix定义了一个回退方法。这种后备方法应该具有与公开服务相同的返回类型。如果暴露服务中出现异常,则回退方法将返回一些值。

    10、什么是Hystrix断路器?我们需要它吗?

    由于某些原因,employee-consumer公开服务会引发异常。在这种情况下使用Hystrix我们定义了一个回退方法。如果在公开服务中发生异常,则回退方法返回一些默认值。

    图片

    如果firstPage method() 中的异常继续发生,则Hystrix电路将中断,并且员工使用者将一起跳过firtsPage方法,并直接调用回退方法。断路器的目的是给第一页方法或第一页方法可能调用的其他方法留出时间,并导致异常恢复。可能发生的情况是,在负载较小的情况下,导致异常的问题有更好的恢复机会 。

    图片

    11、说说 RPC 的实现原理

    首先需要有处理网络连接通讯的模块,负责连接建立、管理和消息的传输。其次需要有编 解码的模块,因为网络通讯都是传输的字节码,需要将我们使用的对象序列化和反序列 化。剩下的就是客户端和服务器端的部分,服务器端暴露要开放的服务接口,客户调用服 务接口的一个代理实现,这个代理实现负责收集数据、编码并传输给服务器然后等待结果 返回。

    12、eureka自我保护机制是什么?

    当Eureka Server 节点在短时间内丢失了过多实例的连接时(比如网络故障或频繁启动关闭客户端)节点会进入自我保护模式,保护注册信息,不再删除注册数据,故障恢复时,自动退出自我保护模式。

    13、什么是Ribbon?

    ribbon是一个负载均衡客户端,可以很好地控制htt和tcp的一些行为。feign默认集成了ribbon

    14、什么是 Netflix Feign?它的优点是什么?

    Feign 是受到 Retrofit,JAXRS-2.0 和 WebSocket 启发的 java 客户端联编程序。

    Feign 的第一个目标是将约束分母的复杂性统一到 http apis,而不考虑其稳定性。

    特点:

    • Feign 采用的是基于接口的注解
    • Feign 整合了ribbon,具有负载均衡的能力
    • 整合了Hystrix,具有熔断的能力

    使用方式

    • 添加pom依赖。
    • 启动类添加@EnableFeignClients
    • 定义一个接口@FeignClient(name=“xxx”)指定调用哪个服务

    15、Ribbon和Feign的区别?

    1.Ribbon都是调用其他服务的,但方式不同。2.启动类注解不同,Ribbon是@RibbonClient feign的是@EnableFeignClients 3.服务指定的位置不同,Ribbon是在@RibbonClient注解上声明,Feign则是在定义抽象方法的接口中使用@FeignClient声明。4.调用方式不同,Ribbon需要自己构建http请求,模拟http请求。

    16、Spring Cloud 的核心组件有哪些?

    • Eureka:服务注册于发现。
    • Feign:基于动态代理机制,根据注解和选择的机器,拼接请求 url 地址,发起请求。
    • Ribbon:实现负载均衡,从一个服务的多台机器中选择一台。
    • Hystrix:提供线程池,不同的服务走不同的线程池,实现了不同服务调用的隔离,避免了服务雪崩的问题。
    • Zuul:网关管理,由 Zuul 网关转发请求给对应的服务。

    17、说说Spring Boot和Spring Cloud的关系

    Spring Boot是Spring推出用于解决传统框架配置文件冗余,装配组件繁杂的基于Maven的解决方案,旨在快速搭建单个微服务 而Spring Cloud专注于解决各个微服务之间的协调与配置,服务之间的通信,熔断,负载均衡等 技术维度并相同,并且Spring Cloud是依赖于Spring Boot的,而Spring Boot并不是依赖与Spring Cloud,甚至还可以和Dubbo进行优秀的整合开发

    总结

    • SpringBoot专注于快速方便的开发单个个体的微服务
    • SpringCloud是关注全局的微服务协调整理治理框架,整合并管理各个微服务,为各个微服务之间提供,配置管理,服务发现,断路器,路由,事件总线等集成服务
    • Spring Boot不依赖于Spring Cloud,Spring Cloud依赖于Spring Boot,属于依赖关系
    • Spring Boot专注于快速,方便的开发单个的微服务个体,Spring Cloud关注全局的服务治理框架

    18、说说微服务之间是如何独立通讯的?

    远程过程调用(Remote Procedure Invocation)

    也就是我们常说的服务的注册与发现,直接通过远程过程调用来访问别的service。

    优点 :简单,常见,因为没有中间件代理,系统更简单

    缺点 :只支持请求/响应的模式,不支持别的,比如通知、请求/异步响应、发布/订阅、发布/异步响应,降低了可用性,因为客户端和服务端在请求过程中必须都是可用的。

    消息

    使用异步消息来做服务间通信。服务间通过消息管道来交换消息,从而通信。

    优点 :把客户端和服务端解耦,更松耦合,提高可用性,因为消息中间件缓存了消息,直到消费者可以消费,   支持很多通信机制比如通知、请求/异步响应、发布/订阅、发布/异步响应。

    缺点 :消息中间件有额外的复杂。

    19、Spring Cloud如何实现服务的注册?

    服务发布时,指定对应的服务名,将服务注册到 注册中心(Eureka 、Zookeeper)

    注册中心加@EnableEurekaServer,服务用@EnableDiscoveryClient,然后用ribbon或feign进行服务直接的调用发现。

    此题偏向于向实战,就看你是不是背面试题的,没有实战的人是不知道的。

    20、什么是服务熔断?

    在复杂的分布式系统中,微服务之间的相互调用,有可能出现各种各样的原因导致服务的阻塞,在高并发场景下,服务的阻塞意味着线程的阻塞,导致当前线程不可用,服务器的线程全部阻塞,导致服务器崩溃,由于服务之间的调用关系是同步的,会对整个微服务系统造成服务雪崩

    为了解决某个微服务的调用响应时间过长或者不可用进而占用越来越多的系统资源引起雪崩效应就需要进行服务熔断和服务降级处理。

    所谓的服务熔断指的是某个服务故障或异常一起类似显示世界中的“保险丝”当某个异常条件被触发就直接熔断整个服务,而不是一直等到此服务超时。

    服务熔断就是相当于我们电闸的保险丝,一旦发生服务雪崩的,就会熔断整个服务,通过维护一个自己的线程池,当线程达到阈值的时候就启动服务降级,如果其他请求继续访问就直接返回fallback的默认值

    21、了解Eureka自我保护机制吗?

    当Eureka Server 节点在短时间内丢失了过多实例的连接时(比如网络故障或频繁启动关闭客户端)节点会进入自我保护模式,保护注册信息,不再删除注册数据,故障恢复时,自动退出自我保护模式。

    22、熟悉 Spring Cloud Bus 吗?

    spring cloud bus 将分布式的节点用轻量的消息代理连接起来,它可以用于广播配置文件的更改或者服务直接的通讯,也可用于监控。如果修改了配置文件,发送一次请求,所有的客户端便会重新读取配置文件。

    23、Spring Cloud 断路器有什么作用?

    当一个服务调用另一个服务由于网络原因或自身原因出现问题,调用者就会等待被调用者的响应,当更多的服务请求到这些资源导致更多的请求等待,发生连锁效应(雪崩效应)。一段时间内 达到一定的次数无法调用 并且多次监测没有恢复的迹象,这时候断路器完全打开 那么下次请求就不会请求到该服务。

    半开:短时间内 有恢复迹象 断路器会将部分请求发给该服务,正常调用时 断路器关闭。关闭:当服务一直处于正常状态 能正常调用。

    24、了解Spring Cloud Config 吗?

    在分布式系统中,由于服务数量巨多,为了方便服务配置文件统一管理,实时更新,所以需要分布式配置中心组件。在Spring Cloud中,有分布式配置中心组件Spring Cloud Config,它支持配置服务放在配置服务的内存中(即本地),也支持放在远程Git仓库中。

    Spring Cloud Config 组件中,分两个角色,一是config server,二是config client。

    使用方式:

    • 添加pom依赖
    • 配置文件添加相关配置
    • 启动类添加注解@EnableConfigServer

    25、说说你对Spring Cloud Gateway的理解

    Spring Cloud Gateway是Spring Cloud官方推出的第二代网关框架,取代Zuul网关。网关作为流量的,在微服务系统中有着非常作用,网关常见的功能有路由转发、权限校验、限流控制等作用。

    使用了一个RouteLocatorBuilder的bean去创建路由,除了创建路由RouteLocatorBuilder可以让你添加各种predicates和filters,predicates断言的意思,顾名思义就是根据具体的请求的规则,由具体的route去处理,filters是各种过滤器,用来对请求做各种判断和修改。

    参考;http://1pgqu.cn/M0NZo

    总结

    Spring Cloud目前相当的火热,也差不多是java开发者必备技能之一了。面试的时候被问,那也是正常不过了,很多人可能用来很久,但是没有去了解过原理,面试照样挂掉。背面试题,在很大层面上还是很有用的。但从长远角度来说,希望大家更深层次去学习、去实践。只有自己真的掌握,那才叫NB。

    
    

  • Springboot代码混淆,别再让代码在线上进行裸奔

    快乐分享,Java干货及时送达👇

    来源:blog.csdn.net/qq_35387940/article/details/127426354

    • 编译
    • 反编译
    • 混淆
    • 正文

    编译

    简单就是把代码跑一哈,然后我们的代码 .java文件 就被编译成了 .class 文件

    图片

    反编译

    就是针对编译生成的 jar/war 包 里面的 .class 文件 逆向还原回来,可以看到你的代码写的啥。

    比较常用的反编译工具 JD-GUI ,直接把编译好的jar丢进去,大部分都能反编译看到源码:

    图片

    那如果不想给别人反编译看自己写的代码呢?

    怎么做?

    混淆

    该篇玩的代码混淆 ,是其中一种手段。

    我给你看,但你反编译看到的不是真正的代码。

    先看一张效果示例图 :

    图片

    开搞

    正文

    先看一下我们混淆一个项目代码,要做啥?

    图片

    一共就两步

    第一步, 在项目路径下,新增一份文件 proguard.cfg :

    proguard.cfg

    #指定Java的版本
    -target 1.8
    #proguard会对代码进行优化压缩,他会删除从未使用的类或者类成员变量等
    -dontshrink
    #是否关闭字节码级别的优化,如果不开启则设置如下配置
    -dontoptimize
    #混淆时不生成大小写混合的类名,默认是可以大小写混合
    -dontusemixedcaseclassnames
    # 对于类成员的命名的混淆采取唯一策略
    -useuniqueclassmembernames
    #混淆时不生成大小写混合的类名,默认是可以大小写混合
    -dontusemixedcaseclassnames
    #混淆类名之后,对使用Class.forName('className')之类的地方进行相应替代
    -adaptclassstrings
     
    #对异常、注解信息予以保留
    -keepattributes Exceptions,InnerClasses,Signature,Deprecated,SourceFile,LineNumberTable,*Annotation*,EnclosingMethod
    # 此选项将保存接口中的所有原始名称(不混淆)-->
    -keepnames interface ** { *; }
    # 此选项将保存所有软件包中的所有原始接口文件(不进行混淆)
    #-keep interface * extends * { *; }
    #保留参数名,因为控制器,或者Mybatis等接口的参数如果混淆会导致无法接受参数,xml文件找不到参数
    -keepparameternames
    # 保留枚举成员及方法
    -keepclassmembers enum * { *; }
    # 不混淆所有类,保存原始定义的注释-
    -keepclassmembers class * {
                            @org.springframework.context.annotation.Bean *;
                            @org.springframework.beans.factory.annotation.Autowired *;
                            @org.springframework.beans.factory.annotation.Value *;
                            @org.springframework.stereotype.Service *;
                            @org.springframework.stereotype.Component *;
                            }
     
    #忽略warn消息
    -ignorewarnings
    #忽略note消息
    -dontnote
    #打印配置信息
    -printconfiguration
    -keep public class com.example.myproguarddemo.MyproguarddemoApplication {
            public static void main(java.lang.String[]);
        }

    注意点:

    图片

    其余的看注释,可以配置哪些类不参与混淆,哪些枚举保留,哪些方法名不混淆等等。

    第二步,在pom文件上 加入proguard 混淆插件 :

    build标签里面改动加入一下配置


        
            
                com.github.wvengen
                proguard-maven-plugin
                2.6.0
                
                    
                    
                        package
                        
                            proguard
                        

                    

                

                
                    
                    ${project.build.finalName}.jar
                    
                    ${project.build.finalName}.jar
                    
                    true
                    
                    ${project.basedir}/proguard.cfg
                    
                    
                        ${java.home}/lib/rt.jar
                        ${java.home}/lib/jce.jar
                        ${java.home}/lib/jsse.jar
                    

                    
                    !META-INF/**,!META-INF/versions/9/**.class
                    
                    ${project.basedir}/target
                    
                    
                        
                    

                

            

            
                org.springframework.boot
                spring-boot-maven-plugin
                
                    
                        
                            repackage
                        

                        
                            com.example.myproguarddemo.MyproguarddemoApplication
                        

                    

                

            

        


    注意点:

    图片

    图片

    然后可以看到:

    图片

    然后点击package,正常执行编译打包流程就可以 :

    图片

    然后可以看到jar的生成:

    图片

    看看效果:

    图片

    好了,该篇就到这。

    
    

  • 腾讯:@Bean 与 @Component 用在同一个类上,会怎么样?

    快乐分享,Java干货及时送达👇

    来源:cnblogs.com/youzhibing/p/15354706.html

    • 疑虑背景
      • 疑虑描述
      • Spring Boot 版本
    • 结果验证
    • 源码解析
    • Spring 升级优化
    • 总结
      • 补充

    疑虑背景

    疑虑描述

    最近,在进行开发的过程中,发现之前的一个写法,类似如下

    图片

    以我的理解,@Configuration@Bean 会创建一个 userName 不为 null 的 UserManager 对象,而 @Component 也会创建一个 userName 为 null 的 UserManager 对象

    那么我们在其他对象中注入 UserManager 对象时,到底注入的是哪个对象?

    因为项目已经上线了很长一段时间了,所以这种写法没有编译报错,运行也没有出问题

    后面去找同事了解下,实际是想让

    图片

    生效,而实际也确实是它生效了

    那么问题来了:Spring 容器中到底有几个 UserManager 类型的对象?

    Spring Boot 版本

    项目中用的 Spring Boot 版本是:2.0.3.RELEASE

    对象的 scope 是默认值,也就是 singleton

    结果验证

    验证方式有很多,可以 debug 跟源码,看看 Spring 容器中到底有几个 UserManager 对象,也可以直接从 UserManager 构造方法下手,看看哪几个构造方法被调用,等等

    我们从构造方法下手,看看 UserManager 到底实例化了几次

    图片

    只有有参构造方法被调用了,无参构造方法岿然不动(根本没被调用)

    既然 UserManager 构造方法只被调用了一次,那么前面的问题:到底注入的是哪个对象

    答案也就清晰了,没得选了呀,只能是 @Configuration@Bean 创建的 userName 不为 null 的 UserManager 对象

    问题又来了:为什么不是 @Component 创建的 userName 为 null 的 UserManager 对象?

    源码解析

    @Configuration@Component 关系很紧密

    图片

    所以@Configuration 能够被 component scan

    其中 ConfigurationClassPostProcessor@Configuration 息息相关,其类继承结构图如下:

    图片

    它实现了 BeanFactoryPostProcessor 接口和 PriorityOrdered 接口,关于 BeanFactoryPostProcessor

    那么我们从 AbstractApplicationContext 的 refresh 方法调用的 invokeBeanFactoryPostProcessors(beanFactory)开始,来跟下源码

    图片

    此时完成了 com.lee.qsl 包下的 component scancom.lee.qsl 包及子包下的 UserConfig 、 UserController 和 UserManager 都被扫描出来

    注意,此刻@Bean 的处理还未开始, UserManager 是通过@Component 而被扫描出来的;此时 Spring 容器中 beanDefinitionMap 中的 UserManager 是这样的

    图片

    接下来一步很重要,与我们想要的答案息息相关

    图片

    图片

    循环递归处理 UserConfig 、 UserController 和 UserManager ,把它们都封装成 ConfigurationClass ,递归扫描 BeanDefinition

    循环完之后,我们来看看 configClasses

    图片

    UserConfig bean 定义信息中 beanMethods 中有一个元素 [BeanMethod:name=userManager,declaringClass=com.lee.qsl.config.UserConfig]

    然后我们接着往下走,来仔细看看答案出现的环节

    是不是有什么发现?@Component 修饰的 UserManager 定义直接被覆盖成了 @Configuration + @Bean 修饰的 UserManager 定义

    Bean 定义类型也由 ScannedGenericBeanDefinition 替换成了 ConfigurationClassBeanDefinition

    后续通过 BeanDefinition 创建实例的时候,创建的自然就是 @Configuration + @Bean 修饰的 UserManager ,也就是会反射调用 UserManager 的有参构造方法

    自此,答案也就清楚了

    Spring 其实给出了提示

    2021-10-03 20:37:33.697  INFO 13600 --- [           
    main] o.s.b.f.s.DefaultListableBeanFactory     : Overriding bean definition for bean 'userManager' with a different definition: replacing [Generic bean: class [com.lee.qsl.manager.UserManager]; scope=singleton; abstract=false; lazyInit=false; autowireMode=0; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=null; factoryMethodName=null; initMethodName=null; destroyMethodName=null; defined in file [D:qsl-projectspring-boot-bean-componenttargetclassescomleeqslmanagerUserManager.class]] with [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=userConfig; factoryMethodName=userManager; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [com/lee/qsl/config/UserConfig.class]]

    只是日志级别是 info ,太不显眼了

    Spring 升级优化

    可能 Spring 团队意识到了 info 级别太不显眼的问题,或者说意识到了直接覆盖的处理方式不太合理

    所以在 Spring 5.1.2.RELEASE (Spring Boot 则是 2.1.0.RELEASE )做出了优化处理

    我们来具体看看

    图片

    启动直接报错,Spring 也给出了提示

    The bean 'userManager', defined in class path resource [com/lee/qsl/config/UserConfig.class], could not be registered. A bean with that name has already been defined in file [D:qsl-projectspring-boot-bean-componenttargetclassescomleeqslmanagerUserManager.class] and overriding is disabled.

    我们来跟下源码,主要看看与 Spring 5.0.7.RELEASE 的区别

    新增了配置项 allowBeanDefinitionOverriding 来控制是否允许 BeanDefinition 覆盖,默认情况下是不允许的

    我们可以在配置文件中配置:spring.main.allow-bean-definition-overriding=true ,允许 BeanDefinition 覆盖

    这种处理方式是更优的,将选择权交给开发人员,而不是自己偷偷的处理,已达到开发者想要的效果

    总结

    Spring 5.0.7.RELEASESpring Boot 2.0.3.RELEASE ) 支持@Configuration + @Bean@Component 同时作用于同一个类

    启动时会给 info 级别的日志提示,同时会将@Configuration + @Bean 修饰的 BeanDefinition 覆盖掉@Component 修饰的 BeanDefinition

    也许 Spring 团队意识到了上述处理不太合适,于是在 Spring 5.1.2.RELEASE 做出了优化处理

    增加了配置项:allowBeanDefinitionOverriding ,将主动权交给了开发者,由开发者自己决定是否允许覆盖

    补充

    关于 allowBeanDefinitionOverriding ,前面讲的不对,后面特意去翻了下源码,补充如下

    Spring 1.2 引进 DefaultListableBeanFactory 的时候就有了 private boolean allowBeanDefinitionOverriding = true;,默认是允许 BeanDefinition 覆盖

    图片

    Spring 4.1.2 引进了 isAllowBeanDefinitionOverriding()方法

    图片

    Spring 自始至终默认都是允许 BeanDefinition 覆盖的,变的是 Spring Boot , Spring Boot 2.1.0 之前没有覆盖 Spring 的 allowBeanDefinitionOverriding 默认值,仍是允许 BeanDefinition 覆盖的

    Spring Boot 2.1.0 中 SpringApplication 定义了私有属性:allowBeanDefinitionOverriding

    图片

    没有显示的指定值,那么默认值就是 false ,之后在 Spring Boot 启动过程中,会用此值覆盖掉 Spring 中的 allowBeanDefinitionOverriding 的默认值

    关于 allowBeanDefinitionOverriding ,我想大家应该已经清楚了

    
    

  • SpringBoot + Druid DataSource 实现监控 MySQL 性能

    快乐分享,Java干货及时送达👇

    来源:blog.csdn.net/lvoelife/article/details/128092586

    1. 基本概念

    我们都使用过连接池,比如C3P0,DBCP,hikari, Druid,虽然HikariCP的速度稍快,但Druid能够提供强大的监控和扩展功能,也是阿里巴巴的开源项目。

    Druid是阿里巴巴开发的号称为监控而生的数据库连接池,在功能、性能、扩展性方面,都超过其他数据库连接池,包括DBCP、C3P0、BoneCP、Proxool、JBoss DataSource等等,秒杀一切。

    Druid可以很好的监控DB池连接和SQL的执行情况,天生就是针对监控而生的DB连接池。

    Spring Boot默认数据源HikariDataSourceJdbcTemplate中已经介绍Spring Boot 2.x默认使用Hikari数据源,可以说Hikari与Driud都是当前Java Web上最优秀的数据源。

    而Druid已经在阿里巴巴部署了超过600个应用,经过好几年生产环境大规模部署的严苛考验!

    • stat: Druid内置提供一个StatFilter,用于统计监控信息。
    • wall: Druid防御SQL注入攻击的WallFilter就是通过Druid的SQL Parser分析。Druid提供的SQL Parser可以在JDBC层拦截SQL做相应处理,比如说分库分表、审计等。
    • log4j2: 这个就是 日志记录的功能,可以把sql语句打印到log4j2供排查问题。

    2. 相关配置

    2.1 添加依赖


        1.8
        1.2.11



        com.alibaba
        druid-spring-boot-starter
        ${alibabaDruidStarter.version}

    2.2 配置属性

    • 配置Druid数据源(连接池): 如同c3p0、dbcp数据源可以设置数据源连接初始化大小、最大连接数、等待时间、最小连接数 等一样,Druid数据源同理可以进行设置。
    • 配置Druid web监控filter(WebStatFilter): 这个过滤器的作用就是统计web应用请求中所有的数据库信息,比如 发出的sql语句,sql执行的时间、请求次数、请求的url地址、以及seesion监控、数据库表的访问次数等等。
    • 配置Druid后台管理Servlet(StatViewServlet): Druid数据源具有监控的功能,并提供了一个web界面方便用户查看,类似安装 路由器 时,人家也提供了一个默认的web页面;需要设置Druid的后台管理页面的属性,比如 登录账号、密码等。

    【注意】:Druid Spring Boot Starter配置属性的名称完全遵照Druid,可以通过Spring Boot配置文件来配置Druid数据库连接池和监控,如果没有配置则使用默认值,如下在application.yml配置相关属性:

    # spring 配置
    spring:
      datasource:
        driver-class-name: com.mysql.cj.jdbc.Driver
        password: 123456
        username: root
        url: jdbc:mysql://localhost:3306/superjson?useUnicode=true&characterEncoding=utf8&useSSL=false
        # 连接池配置
        druid:
          # 初始化大小,最小,最大
          initial-size: 5
          min-idle: 5
          max-active: 20
          # 配置获取连接等待超时的时间
          max-wait: 60000
          # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位毫秒
          time-between-eviction-runs-millis: 60000
          # 配置一个连接在池中最小生存时间
          min-evictable-idle-time-millis: 300000
          validation-query: SELECT 1 FROM user
          test-while-idle: true
          test-on-borrow: false
          test-on-return: false
          # 打开 PSCache,并且指定每个连接上 PSCache 的大小
          pool-prepared-statements: true
          max-pool-prepared-statement-per-connection-size: 20
          # 配置监控统计拦截的 Filter,去掉后监控界面 SQL 无法统计,wall 用于防火墙
          filters: stat,wall,slf4j
          # 通过 connection-properties 属性打开 mergeSql 功能;慢 SQL 记录
          connection-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
          # 配置 DruidStatFilter
          web-stat-filter:
            enabled: true
            url-pattern: /*
            exclusions: .js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico,/druid/*
          # 配置 DruidStatViewServlet
          stat-view-servlet:
            url-pattern: /druid/*
            # IP 白名单,没有配置或者为空,则允许所有访问
            allow: 127.0.0.1
            # IP 黑名单,若白名单也存在,则优先使用
            deny: 192.168.31.253
            # 禁用 HTML 中 Reset All 按钮
            reset-enable: false
            # 登录用户名/密码
            login-username: root
            login-password: 123456
            # 需要设置enabled=true,否则会报出There was an unexpected error (type=Not Found, status=404).错误,或者将druid-spring-boot-starter的版本降低到1.1.10及以下
            # 是否启用StatViewServlet(监控页面)默认值为false(考虑到安全问题默认并未启动,如需启用建议设置密码或白名单以保障安全)
            enabled: true

    上述配置文件的参数可以在com.alibaba.druid.spring.boot.autoconfigure.properties.DruidStatPropertiesorg.springframework.boot.autoconfigure.jdbc.DataSourcePropertie中找到。

    2.3 配置Filter

    可以通过spring.datasource.druid.filters=stat,wall,log4j ...的方式来启用相应的内置Filter,不过这些Filter都是默认配置。如果默认配置不能满足需求,可以放弃这种方式,通过配置文件来配置Filter,如下所示:

    # 配置StatFilter 
    spring.datasource.druid.filter.stat.enabled=true
    spring.datasource.druid.filter.stat.db-type=h2
    spring.datasource.druid.filter.stat.log-slow-sql=true
    spring.datasource.druid.filter.stat.slow-sql-millis=2000

    # 配置WallFilter 
    spring.datasource.druid.filter.wall.enabled=true
    spring.datasource.druid.filter.wall.db-type=h2
    spring.datasource.druid.filter.wall.config.delete-allow=false
    spring.datasource.druid.filter.wall.config.drop-table-allow=false

    目前为以下Filter提供了配置支持,根据(spring.datasource.druid.filter.*)进行配置。

    • StatFilter
    • WallFilter
    • ConfigFilter
    • EncodingConvertFilter
    • Slf4jLogFilter
    • Log4jFilter
    • Log4j2Filter
    • CommonsLogFilter

    不想使用内置的Filters,要想使自定义Filter配置生效需要将对应Filter的enabled设置为true,Druid Spring Boot Starter默认禁用StatFilter,可以将其enabled设置为true来启用它。

    3 监控页面

    1. 启动项目后,访问http://localhost:8081/druid/login.html来到登录页面,输入用户名密码登录,如下所示:

    图片
    1. 数据源页面 是当前DataSource配置的基本信息,上述配置的Filter可以在里面找到,如果没有配置 Filter(一些信息会无法统计,例如SQL监控会无法获取JDBC相关的SQL执行信息)

    图片
    1. SQL监控页面,统计了所有SQL语句的执行情况

    图片
    1. URL监控页面,统计了所有Controller接口的访问以及执行情况

    图片
    1. Spring监控页面,利用aop对指定接口的执行时间,jdbc数进行记录

    图片
    1. SQL防火墙页面

    druid提供了黑白名单的访问,可以清楚的看到sql防护情况。

    1. Session监控页面

    可以看到当前的session状况,创建时间、最后活跃时间、请求次数、请求时间等详细参数。

    1. JSONAPI页面

    通过api的形式访问Druid的监控接口,api接口返回Json形式数据。

    4. sql监控

    配置Druid web监控filter(WebStatFilter)这个过滤器,作用就是统计web应用请求中所有的数据库信息,比如 发出的sql语句,sql执行的时间、请求次数、请求的url地址、以及seesion监控、数据库表的访问次数,如下配置:

    spring:
      datasource:
        druid:
          ########## 配置WebStatFilter,用于采集web关联监控的数据 ##########
          web-stat-filter:
            enabled: true                   # 启动 StatFilter
            url-pattern: /*                 # 过滤所有url
            exclusions: "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*" # 排除一些不必要的url
            session-stat-enable: true       # 开启session统计功能
            session-stat-max-count: 1000    # session的最大个数,默认100

    5. 慢sql记录

    有时候,系统中有些SQL执行很慢,我们希望使用日志记录下来,可以开启Druid的慢SQL记录功能,如下配置:

    spring:
      datasource:
        druid:
          filter:
            stat:
              enabled: true         # 开启DruidDataSource状态监控
              db-type: mysql        # 数据库的类型
              log-slow-sql: true    # 开启慢SQL记录功能
              slow-sql-millis: 2000 # 默认3000毫秒,这里超过2s,就是慢,记录到日志

    启动后,如果遇到执行慢的SQL,便会输出到日志中

    6. spring 监控

    访问之后spring监控默认是没有数据的,但需要导入SprngBoot的AOP的Starter,如下所示:



        org.springframework.boot
        spring-boot-starter-aop

    同时需要在application.yml按如下配置:

    Spring监控AOP切入点,如com.springboot.template.dao.*,配置多个英文逗号分隔

    spring.datasource.druid.aop-patterns="com.springboot.template.dao.*"

    7. 去广告(Ad)

    访问监控页面的时候,你可能会在页面底部(footer)看到阿里巴巴的广告,如下所示:

    图片

    原因:引入的druid的jar包中的common.js(里面有一段js代码是给页面的footer追加广告的)

    如果想去掉,有两种方式:

    1. 直接手动注释这段代码

    如果是使用Maven,直接到本地仓库中,查找这个jar包,注释如下代码:

    // this.buildFooter();

    common.js的位置:

    com/alibaba/druid/1.1.23/druid-1.1.23.jar!/support/http/resources/js/common.js

    2. 使用过滤器过滤

    注册一个过滤器,过滤common.js的请求,使用正则表达式替换相关的广告内容,如下代码所示:

    @Configuration
    @ConditionalOnWebApplication
    @AutoConfigureAfter(DruidDataSourceAutoConfigure.class)
    @ConditionalOnProperty(name = "spring.datasource.druid.stat-view-servlet.enabled",
    havingValue = "true", matchIfMissing = true)
    public class RemoveDruidAdConfig {

        /**
        * 方法名: removeDruidAdFilterRegistrationBean
        * 方法描述 除去页面底部的广告
        * @param properties com.alibaba.druid.spring.boot.autoconfigure.properties.DruidStatProperties
        * @return org.springframework.boot.web.servlet.FilterRegistrationBean
        */
        @Bean
        public FilterRegistrationBean removeDruidAdFilterRegistrationBean(DruidStatProperties properties) {

            // 获取web监控页面的参数
            DruidStatProperties.StatViewServlet config = properties.getStatViewServlet();
            // 提取common.js的配置路径
            String pattern = config.getUrlPattern() != null ? config.getUrlPattern() : "/druid/*";
            String commonJsPattern = pattern.replaceAll("\*""js/common.js");

            final String filePath = "support/http/resources/js/common.js";

            //创建filter进行过滤
            Filter filter = new Filter() {
                @Override
                public void init(FilterConfig filterConfig) throws ServletException {}

                @Override
                public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
                    chain.doFilter(request, response);
                    // 重置缓冲区,响应头不会被重置
                    response.resetBuffer();
                    // 获取common.js
                    String text = Utils.readFromResource(filePath);
                    // 正则替换banner, 除去底部的广告信息
                    text = text.replaceAll("
    "
    "");
                    text = text.replaceAll("powered.*?shrek.wang""");
                    response.getWriter().write(text);
                }

                @Override
                public void destroy() {}
            };

            FilterRegistrationBean registrationBean = new FilterRegistrationBean();
            registrationBean.setFilter(filter);
            registrationBean.addUrlPatterns(commonJsPattern);
            return registrationBean;
        }
    }

    两种方式都可以,建议使用的是第一种,从根源解决。

    8. 获取 Druid 的监控数据

    Druid的监控数据可以在开启StatFilter后,通过DruidStatManagerFacade进行获取;

    DruidStatManagerFacade#getDataSourceStatDataList该方法可以获取所有数据源的监控数据,除此之外DruidStatManagerFacade还提供了一些其他方法,可以按需选择使用。

    @RestController
    @RequestMapping(value = "/druid")
    public class DruidStatController {

        @GetMapping("/stat")
        public Object druidStat(){
            // 获取数据源的监控数据
            return DruidStatManagerFacade.getInstance().getDataSourceStatDataList();
        }
    }
    
    

  • SpringBoot 部署打包成 jar 和 war 有什么不同?

    快乐分享,Java干货及时送达👇

    首先给大家来讲一个我们遇到的一个奇怪的问题:

    1. 我的一个springboot项目,用mvn install打包成jar,换一台有jdk的机器就直接可以用java -jar 项目名.jar的方式运行,没任何问题,为什么这里不需要tomcat也可以运行了?

    2. 然后我打包成war放进tomcat运行,发现端口号变成tomcat默认的8080(我在server.port中设置端口8090)项目名称也必须加上了。

    也就是说我在原来的机器的IDEA中运行,项目接口地址为 ip:8090/listall,打包放进另一台机器的tomcat就变成了ip:8080/项目名/listall。这又是为什么呢?

    • 通过jar运行实际上是启动了内置的tomcat,所以用的是应用的配置文件中的端口

    • 直接部署到tomcat之后,内置的tomcat就不会启用,所以相关配置就以安装的tomcat为准,与应用的配置文件就没有关系了

    哎,现在学编程的基本都不会教历史了,也没人有兴趣去钻研。

    总体来说吧,很多年前,Sun 还在世的那个年代,在度过了早期用 C++写 Html 解析器的蛮荒时期后,有一批最早的脚本程序进入了 cgi 时代,此时的 Sun 决定进军这个领域,为了以示区别并显得自己高大上,于是研发了 servlet 标准,搞出了最早的 jsp。并给自己起了个高大上的称号 JavaEE ( Java 企业级应用标准,其实不就是一堆服务器以 http 提供服务吗,吹逼)。

    既然是企业级标准那自然得有自己的服务器标准。于是 Servlet 标准诞生,以此标准实现的服务器称为 Servle 容器服务器,Tomcat 就是其中代表,被 Sun 捐献给了 Apache 基金会,那个时候的 Web 服务器还是个高大上的概念,当时的 Java Web 程序的标准就是 War 包(其实就是个 Zip 包),这就是 War 包的由来。

    后来随着服务器领域的屡次进化,人们发现我们为什么要这么笨重的 Web 服务器,还要实现一大堆 Servlet 之外的管理功能,简化一下抽出核心概念 servlet 不是更好吗,最早这么干的似乎是 Jetty,出现了可以内嵌的 Servelet 服务器。

    去掉了一大堆非核心功能。后来 tomcat 也跟进了,再后来,本来很笨重的传统 JavaEE 服务器 Jboss 也搞了个 undertow 来凑热闹。正好这个时候微服务的概念兴起,“ use Jar,not War ”。要求淘汰传统 Servlet 服务器的呼声就起来了。

    jar包和war包的区别

    1、war是一个web模块,其中需要包括WEB-INF,是可以直接运行的WEB模块;jar一般只是包括一些class文件,在声明了Main_class之后是可以用java命令运行的。

    2、war包是做好一个web应用后,通常是网站,打成包部署到容器中;jar包通常是开发时要引用通用类,打成包便于存放管理。

    3、war是Sun提出的一种Web应用程序格式,也是许多文件的一个压缩包。这个包中的文件按一定目录结构来组织;classes目录下则包含编译好的Servlet类和Jsp或Servlet所依赖的其它类(如JavaBean)可以打包成jar放到WEB-INF下的lib目录下。

    JAR文件格式以流行的ZIP文件格式为基础。与ZIP文件不同的是,JAR 文件不仅用于压缩和发布,而且还用于部署和封装库、组件和插件程序,并可被像编译器和 JVM 这样的工具直接使用。

    【格式特点】:
    • 安全性 可以对 JAR 文件内容加上数字化签名。这样,能够识别签名的工具就可以有选择地为您授予软件安全特权,这是其他文件做不到的,它还可以检测代码是否被篡改过。

    • 减少下载时间 如果一个 applet 捆绑到一个 JAR 文件中,那么浏览器就可以在一个 HTTP 事务中下载这个 applet 的类文件和相关的资源,而不是对每一个文件打开一个新连接。

    • 压缩 JAR 格式允许您压缩文件以提高存储效率。

    • 传输平台扩展 Java 扩展框架(Java Extensions Framework)提供了向 Java 核心平台添加功能的方法,这些扩展是用 JAR 文件打包的(Java 3D 和 JavaMail 就是由 Sun 开发的扩展例子)。

    WAR文件就是一个Web应用程序,建立WAR文件,就是把整个Web应用程序(不包括Web应用程序层次结构的根目录)压缩起来,指定一个war扩展名。

    【建立的条件】:
    • 需要建立正确的Web应用程序的目录层次结构。

    • 建立WEB-INF子目录,并在该目录下建立classes与lib两个子目录。

    • 将Servlet类文件放到WEB-INFclasses目录下,将Web应用程序所使用Java类库文件(即JAR文件)放到WEB-INFlib目录下。

    • 将JSP页面或静态HTML页面放到上下文根路径下或其子目录下。

    • 建立META-INF目录,并在该目录下建立context.xml文件。

    下面给大家讲讲怎么将springboot项目打包成jar和war

    SpringBoot项目打包成jar很简单,也是SpringBoot的常用打包格式;本篇博客将SpringBoot打包成jar和war两种方式都记录下来;

    先介绍将SpringBoot打包成jar包的方式:(以下示例是在idea中演示)

    一、打包成jar

    1)先new 一个Spring Starter Project

    这里注意packaging默认为jar,不用修改.

    2)创建完成后项目的pom如下:


    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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
     modelVersion>4.0.0modelVersion>
     parent>
      groupId>org.springframework.bootgroupId>
      artifactId>spring-boot-starter-parentartifactId>
      version>2.1.4.RELEASEversion>
      relativePath/> 
     parent>
     groupId>com.examplegroupId>
     artifactId>demoartifactId>
     version>0.0.1-SNAPSHOTversion>
     name>demoname>
     description>Demo project for Spring Bootdescription>
     
     properties>
      java.version>1.8java.version>
     properties>
     
     dependencies>
      dependency>
       groupId>org.springframework.bootgroupId>
       artifactId>spring-boot-starterartifactId>
      dependency>
     
      dependency>
       groupId>org.springframework.bootgroupId>
       artifactId>spring-boot-starter-testartifactId>
       scope>testscope>
      dependency>
     dependencies>
     
     build>
      plugins>
       plugin>
        groupId>org.springframework.bootgroupId>
        artifactId>spring-boot-maven-pluginartifactId>
       plugin>
      plugins>
     build>
     
    project>

    3)打成jar包(通过maven命令的方式):

    在Terminal窗口,使用 mvn clean package 命令打包:

    然后在target目录下就能看到打包好的jar包了

    二、打包成war包形式

    1)可以在刚才创建的项目上做改动,首先打包成war需要一个ServletInitializer类,这个类的位置需要和启动类在同一个文件下

    如果一开始选择war包形式,会自动创建此类

    2)修改pom.xml

    修改pom.xml的war将原先的jar改为war;

    3)如果我们的SpringBoot是使用html作为前端页面开发没有问题,但是如果我们想用jsp开发,这个时候就需要配置一些依赖了:主要是排除SpringBoot的内置Tomcat,添加javax.servlet-apitomcat-servlet-api(SpringMVC还需要配置后缀);

    最后的pom.xml如下:


    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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
     modelVersion>4.0.0modelVersion>
     parent>
      groupId>org.springframework.bootgroupId>
      artifactId>spring-boot-starter-parentartifactId>
      version>2.1.4.RELEASEversion>
      relativePath/> 
     parent>
     groupId>com.examplegroupId>
     artifactId>demoartifactId>
     version>0.0.1-SNAPSHOTversion>
     packaging>warpackaging>
     name>demoname>
     description>Demo project for Spring Bootdescription>
     
     properties>
      java.version>1.8java.version>
     properties>
     
     dependencies>
            dependency>
                groupId>org.springframework.bootgroupId>
                artifactId>spring-boot-starter-thymeleafartifactId>
                exclusions>
                    exclusion>
                        groupId>org.springframework.bootgroupId>
                        artifactId>spring-boot-starter-tomcatartifactId>
                    exclusion>
                exclusions>
            dependency>
            dependency>
                groupId>javax.servletgroupId>
                artifactId>javax.servlet-apiartifactId>
                scope>providedscope>
            dependency>
     
            dependency>
                groupId>org.apache.tomcatgroupId>
                artifactId>tomcat-servlet-apiartifactId>
                version>8.0.36version>
                scope>providedscope>
            dependency>
     
            dependency>
                groupId>org.springframework.bootgroupId>
                artifactId>spring-boot-starter-webartifactId>
            dependency>
     
            dependency>
                groupId>org.springframework.bootgroupId>
                artifactId>spring-boot-starter-tomcatartifactId>
                scope>providedscope>
            dependency>
     
      dependency>
       groupId>org.springframework.bootgroupId>
       artifactId>spring-boot-starter-testartifactId>
       scope>testscope>
      dependency>
     dependencies>
     
     build>
      plugins>
       plugin>
        groupId>org.springframework.bootgroupId>
        artifactId>spring-boot-maven-pluginartifactId>
       plugin>
      plugins>
     build>
     
    project>

    因为SpringBoot默认推荐的是html,而不是jsp;经过上面的修改就可以使用jsp进行开发了;

    4)打包成war:使用mvn clean package

    如下:

    打包成功后,就可以将war包放在tomcat下的webapps下,然后运行tomcat,启动项目了;

    记录下来,以后用到的时候看 ^_^;

    当然了,在创建项目的时候直接选择package为war,直接就能打成war包了

    当选择war为打包方式创建项目时,ServletInitializer是默认直接创建的

    此时,pom文件如下


    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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
     modelVersion>4.0.0modelVersion>
     parent>
      groupId>org.springframework.bootgroupId>
      artifactId>spring-boot-starter-parentartifactId>
      version>2.1.4.RELEASEversion>
      relativePath/> 
     parent>
     groupId>com.examplegroupId>
     artifactId>demoartifactId>
     version>0.0.1-SNAPSHOTversion>
     packaging>warpackaging>
     name>demoname>
     description>Demo project for Spring Bootdescription>
     
     properties>
      java.version>1.8java.version>
     properties>
     
     dependencies>
      dependency>
       groupId>org.springframework.bootgroupId>
       artifactId>spring-boot-starter-webartifactId>
      dependency>
     
      dependency>
       groupId>org.springframework.bootgroupId>
       artifactId>spring-boot-starter-tomcatartifactId>
       scope>providedscope>
      dependency>
      dependency>
       groupId>org.springframework.bootgroupId>
       artifactId>spring-boot-starter-testartifactId>
       scope>testscope>
      dependency>
     dependencies>
     
     build>
      plugins>
       plugin>
        groupId>org.springframework.bootgroupId>
        artifactId>spring-boot-maven-pluginartifactId>
       plugin>
      plugins>
     build>
     
    project>

    直接mvn clean package就能打包成功

    感谢阅读,希望对你有所帮助 🙂 

    来源:https://blog.csdn.net/weixin_40910372/

  • SpringBoot 生产中 16 条最佳实践

    快乐分享,Java干货及时送达👇

    来源:e4developer.com/2018/08/06/

    • 1、使用自定义 BOM 来维护第三方依赖

    • 2、使用自动配置

    • 3、使用 Spring Initializr 来开始一个新的 Spring Boot 项目

    • 4、考虑为常见的组织问题创建自己的自动配置

    • 5、正确设计代码目录结构

    • 6、保持 @Controller 的简洁和专注

    • 7、围绕业务功能构建 @Service

    • 8、使数据库独立于核心业务逻辑之外

    • 9、保持业务逻辑不受 Spring Boot 代码的影响

    • 10、推荐使用构造函数注入

    • 11、熟悉并发模型

    • 12、加强配置管理的外部化

    • 13、提供全局异常处理

    • 14、使用日志框架

    • 15、测试你的代码

    • 16、使用测试切片让测试更容易,并且更专注

    • 总结


    Spring Boot 是最流行的用于开发微服务的 Java 框架。在本文中,我将与你分享自 2016 年以来我在专业开发中使用 Spring Boot 所采用的最佳实践。这些内容是基于我的个人经验和一些熟知的 Spring Boot 专家的文章。

    在本文中,我将重点介绍 Spring Boot 特有的实践(大多数时候,也适用于 Spring 项目)。以下依次列出了最佳实践,排名不分先后。

    1、使用自定义 BOM 来维护第三方依赖

    这条实践是我根据实际项目中的经历总结出的。

    Spring Boot 项目本身使用和集成了大量的开源项目,它帮助我们维护了这些第三方依赖。但是也有一部分在实际项目使用中并没有包括进来,这就需要我们在项目中自己维护版本。如果在一个大型的项目中,包括了很多未开发模块,那么维护起来就非常的繁琐。

    怎么办呢?事实上,Spring IO Platform 就是做的这个事情,它本身就是 Spring Boot 的子项目,同时维护了其他第三方开源库。我们可以借鉴 Spring IO Platform 来编写自己的基础项目 platform-bom,所有的业务模块项目应该以 BOM 的方式引入。这样在升级第三方依赖时,就只需要升级这一个依赖的版本而已。


       
           
               io.spring.platform
               platform-bom
               Cairo-SR3
               type>pomtype>
               import
           

       


    2、使用自动配置

    Spring Boot 的一个主要特性是使用自动配置。这是 Spring Boot 的一部分,它可以简化你的代码并使之工作。当在类路径上检测到特定的 jar 文件时,自动配置就会被激活。

    使用它的最简单方法是依赖 Spring Boot Starters。因此,如果你想与 Redis 进行集成,你可以首先包括:


       org.springframework.boot
       spring-boot-starter-data-redis

    如果你想与 MongoDB 进行集成,需要这样:


       org.springframework.boot
       spring-boot-starter-data-mongodb

    借助于这些 starters,这些繁琐的配置就可以很好地集成起来并协同工作,而且它们都是经过测试和验证的。这非常有助于避免可怕的 Jar 地狱。

    https://dzone.com/articles/what-is-jar-hell

    通过使用以下注解属性,可以从自动配置中排除某些配置类:

    @EnableAutoConfiguration(exclude = {ClassNotToAutoconfigure.class})

    但只有在绝对必要时才应该这样做。

    有关自动配置的官方文档可在此处找到:

    https://docs.spring.io/spring-boot/docs/current/reference/html/using-boot-auto-configuration.html。

    3、使用 Spring Initializr 来开始一个新的 Spring Boot 项目

    这一条最佳实践来自 Josh Long (Spring Advocate,@starbuxman)。

    Spring Initializr 提供了一个超级简单的方法来创建一个新的 Spring Boot 项目,并根据你的需要来加载可能使用到的依赖。

    https://start.spring.io/

    使用 Initializr 创建应用程序可确保你获得经过测试和验证的依赖项,这些依赖项适用于 Spring 自动配置。你甚至可能会发现一些新的集成,但你可能并没有意识到这些。

    4、考虑为常见的组织问题创建自己的自动配置

    这一条也来自 Josh Long(Spring Advocate,@starbuxman)——这个实践是针对高级用户的。

    如果你在一个严重依赖 Spring Boot 的公司或团队中工作,并且有共同的问题需要解决,那么你可以创建自己的自动配置。

    这项任务涉及较多工作,因此你需要考虑何时获益是值得投入的。与多个略有不同的定制配置相比,维护单个自动配置更容易。

    如果将这个提供 Spring Boot 配置以开源库的形式发布出去,那么将极大地简化数千个用户的配置工作。

    5、正确设计代码目录结构

    尽管允许你有很大的自由,但是有一些基本规则值得遵守来设计你的源代码结构。

    避免使用默认包。确保所有内容(包括你的入口点)都位于一个名称很好的包中,这样就可以避免与装配和组件扫描相关的意外情况;

    将 Application.java(应用的入口类)保留在顶级源代码目录中;

    我建议将控制器和服务放在以功能为导向的模块中,但这是可选的。一些非常好的开发人员建议将所有控制器放在一起。不论怎样,坚持一种风格!

    6、保持 @Controller 的简洁和专注

    Controller 应该非常简单。你可以在此处阅读有关 GRASP 中有关控制器模式部分的说明。你希望控制器作为协调和委派的角色,而不是执行实际的业务逻辑。以下是主要做法:

    https://en.wikipedia.org/wiki/GRASP_(object-oriented_design)#Controller

    控制器应该是无状态的!默认情况下,控制器是单例,并且任何状态都可能导致大量问题;

    控制器不应该执行业务逻辑,而是依赖委托;

    控制器应该处理应用程序的 HTTP 层,这不应该传递给服务;

    控制器应该围绕用例 / 业务能力来设计。

    要深入这个内容,需要进一步地了解设计 REST API 的最佳实践。无论你是否想要使用 Spring Boot,都是值得学习的。

    7、围绕业务功能构建 @Service

    Service 是 Spring Boot 的另一个核心概念。我发现最好围绕业务功能 / 领域 / 用例(无论你怎么称呼都行)来构建服务。

    在应用中设计名称类似AccountService, UserService, PaymentService这样的服务,比起像DatabaseService、ValidationService、CalculationService这样的会更合适一些。

    你可以决定使用 Controler 和 Service 之间的一对一映射,那将是理想的情况。但这并不意味着,Service 之间不能互相调用!

    8、使数据库独立于核心业务逻辑之外

    我之前还不确定如何在 Spring Boot 中最好地处理数据库交互。在阅读了罗伯特 ·C· 马丁的 “Clear Architecture” 之后,对我来说就清晰多了。

    你希望你的数据库逻辑于服务分离出来。理想情况下,你不希望服务知道它正在与哪个数据库通信,这需要一些抽象来封装对象的持久性。

    罗伯特 C. 马丁强烈地说明,你的数据库是一个 “细节”,这意味着不将你的应用程序与特定数据库耦合。过去很少有人会切换数据库,我注意到,使用 Spring Boot 和现代微服务开发会让事情变得更快。

    9、保持业务逻辑不受 Spring Boot 代码的影响

    考虑到 “Clear Architecture” 的教训,你还应该保护你的业务逻辑。将各种 Spring Boot 代码混合在一起是非常诱人的…… 不要这样做。如果你能抵制诱惑,你将保持你的业务逻辑可重用。

    部分服务通常成为库。如果不从代码中删除大量 Spring 注解,则更容易创建。

    10、推荐使用构造函数注入

    这一条实践来自 Phil Webb(Spring Boot 的项目负责人, @phillip_webb)。

    保持业务逻辑免受 Spring Boot 代码侵入的一种方法是使用构造函数注入。不仅是因为@Autowired注解在构造函数上是可选的,而且还可以在没有 Spring 的情况下轻松实例化 bean。

    11、熟悉并发模型

    我写过的最受欢迎的文章之一是 “介绍 Spring Boot 中的并发”。我认为这样做的原因是这个领域经常被误解和忽视。如果使用不当,就会出现问题。

    Introduction to Concurrency in Spring Boot

    在 Spring Boot 中,Controller 和 Service 是默认是单例。如果你不小心,这会引入可能的并发问题。你通常也在处理有限的线程池。请熟悉这些概念。

    如果你正在使用新的 WebFlux 风格的 Spring Boot 应用程序,我已经解释了它在 “Spring’s WebFlux/Reactor Parallelism and Backpressure” 中是如何工作的。

    12、加强配置管理的外部化

    这一点超出了 Spring Boot,虽然这是人们开始创建多个类似服务时常见的问题……

    你可以手动处理 Spring 应用程序的配置。如果你正在处理多个 Spring Boot 应用程序,则需要使配置管理能力更加强大。

    我推荐两种主要方法:

    使用配置服务器,例如 Spring Cloud Config;

    将所有配置存储在环境变量中(可以基于 git 仓库进行配置)。

    这些选项中的任何一个(第二个选项多一些)都要求你在 DevOps 更少工作量,但这在微服务领域是很常见的。

    13、提供全局异常处理

    你真的需要一种处理异常的一致方法。Spring Boot 提供了两种主要方法:

    你应该使用 HandlerExceptionResolver 定义全局异常处理策略;

    你也可以在控制器上添加 @ExceptionHandler 注解,这在某些特定场景下使用可能会很有用。

    这与 Spring 中的几乎相同,并且 Baeldung 有一篇关于 REST 与 Spring 的错误处理的详细文章,非常值得一读。

    https://www.baeldung.com/exception-handling-for-rest-with-spring

    14、使用日志框架

    你可能已经意识到这一点,但你应该使用 Logger 进行日志记录,而不是使用 System.out.println() 手动执行。这很容易在 Spring Boot 中完成,几乎没有配置。只需获取该类的记录器实例:

    Logger logger = LoggerFactory.getLogger(MyClass.class);

    这很重要,因为它可以让你根据需要设置不同的日志记录级别。

    15、测试你的代码

    这不是 Spring Boot 特有的,但它需要提醒——测试你的代码!如果你没有编写测试,那么你将从一开始就编写遗留代码。

    如果有其他人使用你的代码库,那边改变任何东西将会变得危险。当你有多个服务相互依赖时,这甚至可能更具风险。

    由于存在 Spring Boot 最佳实践,因此你应该考虑将 Spring Cloud Contract 用于你的消费者驱动契约,它将使你与其他服务的集成更容易使用。

    16、使用测试切片让测试更容易,并且更专注

    这一条实践来自 Madhura Bhave(Spring 开发者, @madhurabhave23)。

    使用 Spring Boot 测试代码可能很棘手——你需要初始化数据层,连接大量服务,模拟事物…… 实际上并不是那么难!答案是使用测试切片。

    使用测试切片,你可以根据需要仅连接部分应用程序。这可以为你节省大量时间,并确保你的测试不会与未使用的内容相关联。来自 spring.io 的一篇名为 Custom test slice with Spring test 1.4 的博客文章解释了这种技术。

    https://spring.io/blog/2016/08/30/custom-test-slice-with-spring-boot-1-4

    总结

    感谢 Spring Boot,编写基于 Spring 的微服务正变得前所未有的简单。我希望通过这些最佳实践,你的实施过程不仅会变得很快,而且从长远来看也会更加强大和成功。

  • 批处理框架 Spring Batch 这么强,你会用吗?

    快乐分享,Java干货及时送达👇

    文章来源:https://blog.csdn.net/topdeveloperr/article/details/84337956

    spring batch简介


    spring batch是spring提供的一个数据处理框架。企业域中的许多应用程序需要批量处理才能在关键任务环境中执行业务操作。这些业务运营包括:

    • 无需用户交互即可最有效地处理大量信息的自动化,复杂处理。这些操作通常包括基于时间的事件(例如月末计算,通知或通信)。
    • 在非常大的数据集中重复处理复杂业务规则的定期应用(例如,保险利益确定或费率调整)。
    • 集成从内部和外部系统接收的信息,这些信息通常需要以事务方式格式化,验证和处理到记录系统中。批处理用于每天为企业处理数十亿的交易。

    Spring Batch是一个轻量级,全面的批处理框架,旨在开发对企业系统日常运营至关重要的强大批处理应用程序。Spring Batch构建了人们期望的Spring Framework特性(生产力,基于POJO的开发方法和一般易用性),同时使开发人员可以在必要时轻松访问和利用更高级的企业服务。Spring Batch不是一个schuedling的框架。

    Spring Batch提供了可重用的功能,这些功能对于处理大量的数据至关重要,包括记录/跟踪,事务管理,作业处理统计,作业重启,跳过和资源管理。它还提供更高级的技术服务和功能,通过优化和分区技术实现极高容量和高性能的批处理作业。Spring Batch可用于两种简单的用例(例如将文件读入数据库或运行存储过程)以及复杂的大量用例(例如在数据库之间移动大量数据,转换它等等) 上)。大批量批处理作业可以高度可扩展的方式利用该框架来处理大量信息。


    Spring Batch架构介绍


    一个典型的批处理应用程序大致如下:

    • 从数据库,文件或队列中读取大量记录。
    • 以某种方式处理数据。
    • 以修改之后的形式写回数据。

    其对应的示意图如下:

    spring batch的一个总体的架构如下:

    在spring batch中一个job可以定义很多的步骤step,在每一个step里面可以定义其专属的ItemReader用于读取数据,ItemProcesseor用于处理数据,ItemWriter用于写数据,而每一个定义的job则都在JobRepository里面,我们可以通过JobLauncher来启动某一个job。


    Spring Batch核心概念介绍



    下面是一些概念是Spring batch框架中的核心概念。

    1、什么是Job

    Job和Step是spring batch执行批处理任务最为核心的两个概念。

    其中Job是一个封装整个批处理过程的一个概念。Job在spring batch的体系当中只是一个最顶层的一个抽象概念,体现在代码当中则它只是一个最上层的接口,其代码如下:


    /**
     * Batch domain object representing a job. Job is an explicit abstraction
     * representing the configuration of a job specified by a developer. It should
     * be noted that restart policy is applied to the job as a whole and not to a
     * step.
     */

    public interface Job {

     String getName();


     boolean isRestartable();


     void execute(JobExecution execution);


     JobParametersIncrementer getJobParametersIncrementer();


     JobParametersValidator getJobParametersValidator();

    }

    在Job这个接口当中定义了五个方法,它的实现类主要有两种类型的job,一个是simplejob,另一个是flowjob。在spring batch当中,job是最顶层的抽象,除job之外我们还有JobInstance以及JobExecution这两个更加底层的抽象。

    一个job是我们运行的基本单位,它内部由step组成。job本质上可以看成step的一个容器。一个job可以按照指定的逻辑顺序组合step,并提供了我们给所有step设置相同属性的方法,例如一些事件监听,跳过策略。

    Spring Batch以SimpleJob类的形式提供了Job接口的默认简单实现,它在Job之上创建了一些标准功能。一个使用java config的例子代码如下:

    @Bean
    public Job footballJob() {
        return this.jobBuilderFactory.get("footballJob")
                         .start(playerLoad())
                         .next(gameLoad())
                         .next(playerSummarization())
                         .end()
                         .build();
    }

    这个配置的意思是:首先给这个job起了一个名字叫footballJob,接着指定了这个job的三个step,他们分别由方法,playerLoad,gameLoad, playerSummarization实现。

    2、什么是JobInstance

    我们在上文已经提到了JobInstance,他是Job的更加底层的一个抽象,他的定义如下:

    public interface JobInstance {
     /**
      * Get unique id for this JobInstance.
      * @return instance id
      */

     public long getInstanceId();
     /**
      * Get job name.
      * @return value of 'id' attribute from 
      */

     public String getJobName();
    }

    他的方法很简单,一个是返回Job的id,另一个是返回Job的名字。

    JobInstance指的是job运行当中,作业执行过程当中的概念。Instance本就是实例的意思。

    比如说现在有一个批处理的job,它的功能是在一天结束时执行行一次。我们假定这个批处理job的名字为’EndOfDay’。在这个情况下,那么每天就会有一个逻辑意义上的JobInstance, 而我们必须记录job的每次运行的情况。

    3、什么是JobParameters

    在上文当中我们提到了,同一个job每天运行一次的话,那么每天都有一个jobIntsance,但他们的job定义都是一样的,那么我们怎么来区别一个job的不同jobinstance了。不妨先做个猜想,虽然jobinstance的job定义一样,但是他们有的东西就不一样,例如运行时间。

    spring batch中提供的用来标识一个jobinstance的东西是:JobParameters。JobParameters对象包含一组用于启动批处理作业的参数,它可以在运行期间用于识别或甚至用作参考数据。我们假设的运行时间,就可以作为一个JobParameters。

    例如, 我们前面的’EndOfDay’的job现在已经有了两个实例,一个产生于1月1日,另一个产生于1月2日,那么我们就可以定义两个JobParameter对象:一个的参数是01-01, 另一个的参数是01-02。因此,识别一个JobInstance的方法可以定义为:

    因此,我么可以通过Jobparameter来操作正确的JobInstance

    4、什么是JobExecution

    JobExecution指的是单次尝试运行一个我们定义好的Job的代码层面的概念。job的一次执行可能以失败也可能成功。只有当执行成功完成时,给定的与执行相对应的JobInstance才也被视为完成。

    还是以前面描述的EndOfDay的job作为示例,假设第一次运行01-01-2019的JobInstance结果是失败。那么此时如果使用与第一次运行相同的Jobparameter参数(即01-01-2019)作业参数再次运行,那么就会创建一个对应于之前jobInstance的一个新的JobExecution实例,JobInstance仍然只有一个。

    JobExecution的接口定义如下:

    public interface JobExecution {
     /**
      * Get unique id for this JobExecution.
      * @return execution id
      */

     public long getExecutionId();
     /**
      * Get job name.
      * @return value of 'id' attribute from 
      */

     public String getJobName();
     /**
      * Get batch status of this execution.
      * @return batch status value.
      */

     public BatchStatus getBatchStatus();
     /**
      * Get time execution entered STARTED status.
      * @return date (time)
      */

     public Date getStartTime();
     /**
      * Get time execution entered end status: COMPLETED, STOPPED, FAILED
      * @return date (time)
      */

     public Date getEndTime();
     /**
      * Get execution exit status.
      * @return exit status.
      */

     public String getExitStatus();
     /**
      * Get time execution was created.
      * @return date (time)
      */

     public Date getCreateTime();
     /**
      * Get time execution was last updated updated.
      * @return date (time)
      */

     public Date getLastUpdatedTime();
     /**
      * Get job parameters for this execution.
      * @return job parameters
      */

     public Properties getJobParameters();

    }

    每一个方法的注释已经解释的很清楚,这里不再多做解释。只提一下BatchStatus,JobExecution当中提供了一个方法getBatchStatus用于获取一个job某一次特地执行的一个状态。BatchStatus是一个代表job状态的枚举类,其定义如下:

    public enum BatchStatus {STARTING, STARTED, STOPPING,
       STOPPED, FAILED, COMPLETED, ABANDONED }

    这些属性对于一个job的执行来说是非常关键的信息,并且spring batch会将他们持久到数据库当中. 在使用Spring batch的过程当中spring batch会自动创建一些表用于存储一些job相关的信息,用于存储JobExecution的表为batch_job_execution,下面是一个从数据库当中截图的实例:

    5、什么是Step

    每一个Step对象都封装了批处理作业的一个独立的阶段。事实上,每一个Job本质上都是由一个或多个步骤组成。每一个step包含定义和控制实际批处理所需的所有信息。任何特定的内容都由编写Job的开发人员自行决定。一个step可以非常简单也可以非常复杂。例如,一个step的功能是将文件中的数据加载到数据库中,那么基于现在spring batch的支持则几乎不需要写代码。更复杂的step可能具有复杂的业务逻辑,这些逻辑作为处理的一部分。与Job一样,Step具有与JobExecution类似的StepExecution,如下图所示:

    6、什么是StepExecution

    StepExecution表示一次执行Step, 每次运行一个Step时都会创建一个新的StepExecution,类似于JobExecution。但是,某个步骤可能由于其之前的步骤失败而无法执行。且仅当Step实际启动时才会创建StepExecution。

    一次step执行的实例由StepExecution类的对象表示。每个StepExecution都包含对其相应步骤的引用以及JobExecution和事务相关的数据,例如提交和回滚计数以及开始和结束时间。此外,每个步骤执行都包含一个ExecutionContext,其中包含开发人员需要在批处理运行中保留的任何数据,例如重新启动所需的统计信息或状态信息。下面是一个从数据库当中截图的实例:

    7、什么是ExecutionContext

    ExecutionContext即每一个StepExecution 的执行环境。它包含一系列的键值对。我们可以用如下代码获取ExecutionContext

    ExecutionContext ecStep = stepExecution.getExecutionContext();
    ExecutionContext ecJob = jobExecution.getExecutionContext();

    8、什么是JobRepository

    JobRepository是一个用于将上述job,step等概念进行持久化的一个类。它同时给Job和Step以及下文会提到的JobLauncher实现提供CRUD操作。首次启动Job时,将从repository中获取JobExecution,并且在执行批处理的过程中,StepExecution和JobExecution将被存储到repository当中。

    @EnableBatchProcessing注解可以为JobRepository提供自动配置。

    9、什么是JobLauncher

    JobLauncher这个接口的功能非常简单,它是用于启动指定了JobParameters的Job,为什么这里要强调指定了JobParameter,原因其实我们在前面已经提到了,jobparameter和job一起才能组成一次job的执行。下面是代码实例:

    public interface JobLauncher {

    public JobExecution run(Job job, JobParameters jobParameters)
                throws JobExecutionAlreadyRunningException, JobRestartException,
                       JobInstanceAlreadyCompleteException, JobParametersInvalidException
    ;
    }

    上面run方法实现的功能是根据传入的job以及jobparamaters从JobRepository获取一个JobExecution并执行Job。

    10、什么是Item Reader

    ItemReader是一个读数据的抽象,它的功能是为每一个Step提供数据输入。当ItemReader以及读完所有数据时,它会返回null来告诉后续操作数据已经读完。Spring Batch为ItemReader提供了非常多的有用的实现类,比如JdbcPagingItemReader,JdbcCursorItemReader等等。

    ItemReader支持的读入的数据源也是非常丰富的,包括各种类型的数据库,文件,数据流,等等。几乎涵盖了我们的所有场景。

    下面是一个JdbcPagingItemReader的例子代码:

    @Bean
    public JdbcPagingItemReader itemReader(DataSource dataSource, PagingQueryProvider queryProvider) {
            Map parameterValues = new HashMap();
            parameterValues.put("status""NEW");

            return new JdbcPagingItemReaderBuilder()
                                               .name("creditReader")
                                               .dataSource(dataSource)
                                               .queryProvider(queryProvider)
                                               .parameterValues(parameterValues)
                                               .rowMapper(customerCreditMapper())
                                               .pageSize(1000)
                                               .build();
    }

    @Bean
    public SqlPagingQueryProviderFactoryBean queryProvider() {
            SqlPagingQueryProviderFactoryBean provider = new SqlPagingQueryProviderFactoryBean();

            provider.setSelectClause("select id, name, credit");
            provider.setFromClause("from customer");
            provider.setWhereClause("where status=:status");
            provider.setSortKey("id");

            return provider;
    }

    JdbcPagingItemReader必须指定一个PagingQueryProvider,负责提供SQL查询语句来按分页返回数据。

    下面是一个JdbcCursorItemReader的例子代码:

     private JdbcCursorItemReader> buildItemReader(final DataSource dataSource, String tableName,
                String tenant) {

            JdbcCursorItemReader> itemReader = new JdbcCursorItemReader();
            itemReader.setDataSource(dataSource);
            itemReader.setSql("sql here");
            itemReader.setRowMapper(new RowMapper());
            return itemReader;
        }

    11、什么是Item Writer

    既然ItemReader是读数据的一个抽象,那么ItemWriter自然就是一个写数据的抽象,它是为每一个step提供数据写出的功能。写的单位是可以配置的,我们可以一次写一条数据,也可以一次写一个chunk的数据,关于chunk下文会有专门的介绍。ItemWriter对于读入的数据是不能做任何操作的。

    Spring Batch为ItemWriter也提供了非常多的有用的实现类,当然我们也可以去实现自己的writer功能。

    12、什么是Item Processor

    ItemProcessor对项目的业务逻辑处理的一个抽象, 当ItemReader读取到一条记录之后,ItemWriter还未写入这条记录之前,I我们可以借助temProcessor提供一个处理业务逻辑的功能,并对数据进行相应操作。如果我们在ItemProcessor发现一条数据不应该被写入,可以通过返回null来表示。ItemProcessor和ItemReader以及ItemWriter可以非常好的结合在一起工作,他们之间的数据传输也非常方便。我们直接使用即可。


    chunk 处理流程


    spring batch提供了让我们按照chunk处理数据的能力,一个chunk的示意图如下:

    它的意思就和图示的一样,由于我们一次batch的任务可能会有很多的数据读写操作,因此一条一条的处理并向数据库提交的话效率不会很高,因此spring batch提供了chunk这个概念,我们可以设定一个chunk size,spring batch 将一条一条处理数据,但不提交到数据库,只有当处理的数据数量达到chunk size设定的值得时候,才一起去commit.

    java的实例定义代码如下:

    在上面这个step里面,chunk size被设为了10,当ItemReader读的数据数量达到10的时候,这一批次的数据就一起被传到itemWriter,同时transaction被提交。

    1、skip策略和失败处理

    一个batch的job的step,可能会处理非常大数量的数据,难免会遇到出错的情况,出错的情况虽出现的概率较小,但是我们不得不考虑这些情况,因为我们做数据迁移最重要的是要保证数据的最终一致性。spring batch当然也考虑到了这种情况,并且为我们提供了相关的技术支持,请看如下bean的配置:

    我们需要留意这三个方法,分别是skipLimit(),skip(),noSkip(),

    skipLimit方法的意思是我们可以设定一个我们允许的这个step可以跳过的异常数量,假如我们设定为10,则当这个step运行时,只要出现的异常数目不超过10,整个step都不会fail。注意,若不设定skipLimit,则其默认值是0.

    skip方法我们可以指定我们可以跳过的异常,因为有些异常的出现,我们是可以忽略的。

    noSkip方法的意思则是指出现这个异常我们不想跳过,也就是从skip的所以exception当中排除这个exception,从上面的例子来说,也就是跳过所有除FileNotFoundException的exception。那么对于这个step来说,FileNotFoundException就是一个fatal的exception,抛出这个exception的时候step就会直接fail


    批处理操作指南


    本部分是一些使用spring batch时的值得注意的点


    1、批处理原则

    在构建批处理解决方案时,应考虑以下关键原则和注意事项。

    • 批处理体系结构通常会影响体系结构

    • 尽可能简化并避免在单批应用程序中构建复杂的逻辑结构

    • 保持数据的处理和存储在物理上靠得很近(换句话说,将数据保存在处理过程中)。

    • 最大限度地减少系统资源的使用,尤其是I / O. 在internal memory中执行尽可能多的操作。

    • 查看应用程序I / O(分析SQL语句)以确保避免不必要的物理I / O. 特别是,需要寻找以下四个常见缺陷:

    1. 当数据可以被读取一次并缓存或保存在工作存储中时,读取每个事务的数据。

    2. 重新读取先前在同一事务中读取数据的事务的数据。

    3. 导致不必要的表或索引扫描。

    4. 未在SQL语句的WHERE子句中指定键值。

  • 在批处理运行中不要做两次一样的事情。例如,如果需要数据汇总以用于报告目的,则应该(如果可能)在最初处理数据时递增存储的总计,因此您的报告应用程序不必重新处理相同的数据。

  • 在批处理应用程序开始时分配足够的内存,以避免在此过程中进行耗时的重新分配。

  • 总是假设数据完整性最差。插入适当的检查和记录验证以维护数据完整性。

  • 尽可能实施校验和以进行内部验证。例如,对于一个文件里的数据应该有一个数据条数纪录,告诉文件中的记录总数以及关键字段的汇总。

  • 在具有真实数据量的类似生产环境中尽早计划和执行压力测试。

  • 在大批量系统中,数据备份可能具有挑战性,特别是如果系统以24-7在线的情况运行。数据库备份通常在在线设计中得到很好的处理,但文件备份应该被视为同样重要。如果系统依赖于文件,则文件备份过程不仅应该到位并记录在案,还应定期进行测试。

  • 2、如何默认不启动job

    在使用java config使用spring batch的job时,如果不做任何配置,项目在启动时就会默认去跑我们定义好的批处理job。那么如何让项目在启动时不自动去跑job呢?

    spring batch的job会在项目启动时自动run,如果我们不想让他在启动时run的话,可以在application.properties中添加如下属性:

    spring.batch.job.enabled=false


    3、在读数据时内存不够

    在使用spring batch做数据迁移时,发现在job启动后,执行到一定时间点时就卡在一个地方不动了,且log也不再打印,等待一段时间之后,得到如下错误:

    红字的信息为:Resource exhaustion event:the JVM was unable to allocate memory from the heap.

    翻译过来的意思就是项目发出了一个资源耗尽的事件,告诉我们java虚拟机无法再为堆分配内存。

    造成这个错误的原因是: 这个项目里的batch job的reader是一次性拿回了数据库里的所有数据,并没有进行分页,当这个数据量太大时,就会导致内存不够用。解决的办法有两个:

    • 调整reader读数据逻辑,按分页读取,但实现上会麻烦一些,且运行效率会下降
    • 增大service内存
    
    

  • SpringBoot + K8S 中的滚动发布、优雅停机、弹性伸缩、应用监控、配置分离

    快乐分享,Java干货及时送达👇

    来源:blog.csdn.net/qq_14999375/article/details/123309636

    • 前言

    • 配置

      • 健康检查

      • 滚动更新

      • 弹性伸缩

      • Prometheus集成

      • 配置分离

    • 汇总配置

      • 业务层面

      • 运维层面


    前言

    K8s + SpringBoot实现零宕机发布:健康检查+滚动更新+优雅停机+弹性伸缩+Prometheus监控+配置分离(镜像复用)

    配置

    健康检查

    • 健康检查类型:就绪探针(readiness)+ 存活探针(liveness)
    • 探针类型:exec(进入容器执行脚本)、tcpSocket(探测端口)、httpGet(调用接口)
    业务层面

    项目依赖 pom.xml


        org.springframework.boot
        spring-boot-starter-actuator

    定义访问端口、路径及权限 application.yaml

    management:
      server:
        port: 50000                         # 启用独立运维端口
      endpoint:                             # 开启health端点
        health:
          probes:
            enabled: true
      endpoints:
        web:
          exposure:
            base-path: /actuator            # 指定上下文路径,启用相应端点
            include: health

    将暴露/actuator/health/readiness/actuator/health/liveness两个接口,访问方式如下:

    http://127.0.0.1:50000/actuator/health/readiness
    http://127.0.0.1:50000/actuator/health/liveness
    运维层面

    k8s部署模版deployment.yaml

    apiVersion: apps/v1
    kind: Deployment
    spec:
      template:
        spec:
          containers:
          - name: {APP_NAME}
            image: {IMAGE_URL}
            imagePullPolicy: Always
            ports:
            - containerPort: {APP_PORT}
            - name: management-port
              containerPort: 50000         # 应用管理端口
            readinessProbe:                # 就绪探针
              httpGet:
                path: /actuator/health/readiness
                port: management-port
              initialDelaySeconds: 30      # 延迟加载时间
              periodSeconds: 10            # 重试时间间隔
              timeoutSeconds: 1            # 超时时间设置
              successThreshold: 1          # 健康阈值
              failureThreshold: 6          # 不健康阈值
            livenessProbe:                 # 存活探针
              httpGet:
                path: /actuator/health/liveness
                port: management-port
              initialDelaySeconds: 30      # 延迟加载时间
              periodSeconds: 10            # 重试时间间隔
              timeoutSeconds: 1            # 超时时间设置
              successThreshold: 1          # 健康阈值
              failureThreshold: 6          # 不健康阈值

    滚动更新

    k8s资源调度之滚动更新策略,若要实现零宕机发布,需支持健康检查

    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: {APP_NAME}
      labels:
        app: {APP_NAME}
    spec:
      selector:
        matchLabels:
          app: {APP_NAME}
      replicas: {REPLICAS}    # Pod副本数
      strategy:
        type: RollingUpdate    # 滚动更新策略
        rollingUpdate:
          maxSurge: 1                   # 升级过程中最多可以比原先设置的副本数多出的数量
          maxUnavailable: 1             # 升级过程中最多有多少个POD处于无法提供服务的状态
    优雅停机

    在K8s中,当我们实现滚动升级之前,务必要实现应用级别的优雅停机。否则滚动升级时,还是会影响到业务。使应用关闭线程、释放连接资源后再停止服务

    业务层面

    项目依赖 pom.xml


        org.springframework.boot
        spring-boot-starter-actuator

    定义访问端口、路径及权限 application.yaml

    spring:
      application:
        name: 
      profiles:
        active: @profileActive@
      lifecycle:
        timeout-per-shutdown-phase: 30s     # 停机过程超时时长设置30s,超过30s,直接停机

    server:
      port: 8080
      shutdown: graceful                    # 默认为IMMEDIATE,表示立即关机;GRACEFUL表示优雅关机

    management:
      server:
        port: 50000                         # 启用独立运维端口
      endpoint:                             # 开启shutdown和health端点
        shutdown:
          enabled: true
        health:
          probes:
            enabled: true
      endpoints:
        web:
          exposure:
            base-path: /actuator            # 指定上下文路径,启用相应端点
            include: health,shutdown

    将暴露/actuator/shutdown接口,调用方式如下:

    curl -X POST 127.0.0.1:50000/actuator/shutdown
    运维层面

    确保dockerfile模版集成curl工具,否则无法使用curl命令

    FROM openjdk:8-jdk-alpine
    #构建参数
    ARG JAR_FILE
    ARG WORK_PATH="/app"
    ARG EXPOSE_PORT=8080

    #环境变量
    ENV JAVA_OPTS=""
        JAR_FILE=${JAR_FILE}

    #设置时区
    RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && echo 'Asia/Shanghai' >/etc/timezone
    RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories  
        && apk add --no-cache curl
    #将maven目录的jar包拷贝到docker中,并命名为for_docker.jar
    COPY target/$JAR_FILE $WORK_PATH/


    #设置工作目录
    WORKDIR $WORK_PATH


    # 指定于外界交互的端口
    EXPOSE $EXPOSE_PORT
    # 配置容器,使其可执行化
    ENTRYPOINT exec java $JAVA_OPTS -jar $JAR_FILE

    k8s部署模版deployment.yaml

    注:经验证,java项目可省略结束回调钩子的配置

    此外,若需使用回调钩子,需保证镜像中包含curl工具,且需注意应用管理端口(50000)不能暴露到公网

    apiVersion: apps/v1
    kind: Deployment
    spec:
      template:
        spec:
          containers:
          - name: {APP_NAME}
            image: {IMAGE_URL}
            imagePullPolicy: Always
            ports:
            - containerPort: {APP_PORT}
            - containerPort: 50000
            lifecycle:
              preStop:       # 结束回调钩子
                exec:
                  command: ["curl""-XPOST""127.0.0.1:50000/actuator/shutdown"]

    弹性伸缩

    为pod设置资源限制后,创建HPA

    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: {APP_NAME}
      labels:
        app: {APP_NAME}
    spec:
      template:
        spec:
          containers:
          - name: {APP_NAME}
            image: {IMAGE_URL}
            imagePullPolicy: Always
            resources:                     # 容器资源管理
              limits:                      # 资源限制(监控使用情况)
                cpu: 0.5
                memory: 1Gi
              requests:                    # 最小可用资源(灵活调度)
                cpu: 0.15
                memory: 300Mi
    ---
    kind: HorizontalPodAutoscaler            # 弹性伸缩控制器
    apiVersion: autoscaling/v2beta2
    metadata:
      name: {APP_NAME}
    spec:
      scaleTargetRef:
        apiVersion: apps/v1
        kind: Deployment
        name: {APP_NAME}
      minReplicas: {REPLICAS}                # 缩放范围
      maxReplicas: 6
      metrics:
        - type: Resource
          resource:
            name: cpu                        # 指定资源指标
            target:
              type: Utilization
              averageUtilization: 50

    Prometheus集成

    业务层面

    项目依赖 pom.xml



        org.springframework.boot
        spring-boot-starter-actuator


        io.micrometer
        micrometer-registry-prometheus

    定义访问端口、路径及权限 application.yaml

    management:
      server:
        port: 50000                         # 启用独立运维端口
      metrics:
        tags:
          application: ${spring.application.name}
      endpoints:
        web:
          exposure:
            base-path: /actuator            # 指定上下文路径,启用相应端点
            include: metrics,prometheus

    将暴露/actuator/metric/actuator/prometheus接口,访问方式如下:

    http://127.0.0.1:50000/actuator/metric
    http://127.0.0.1:50000/actuator/prometheus
    运维层面

    deployment.yaml

    apiVersion: apps/v1
    kind: Deployment
    spec:
      template:
        metadata:
          annotations:
            prometheus:io/port: "50000"
            prometheus.io/path: /actuator/prometheus  # 在流水线中赋值
            prometheus.io/scrape: "true"              # 基于pod的服务发现

    配置分离

    方案:通过configmap挂载外部配置文件,并指定激活环境运行

    作用:配置分离,避免敏感信息泄露;镜像复用,提高交付效率

    通过文件生成configmap

    # 通过dry-run的方式生成yaml文件
    kubectl create cm -n   --from-file=application-test.yaml --dry-run=1 -oyaml > configmap.yaml

    # 更新
    kubectl apply -f configmap.yaml

    挂载configmap并指定激活环境

    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: {APP_NAME}
      labels:
        app: {APP_NAME}
    spec:
      template:
        spec:
          containers:
          - name: {APP_NAME}
            image: {IMAGE_URL}
            imagePullPolicy: Always
            env:
              - name: SPRING_PROFILES_ACTIVE   # 指定激活环境
                value: test
            volumeMounts:                      # 挂载configmap
            - name: conf
              mountPath: "/app/config"         # 与Dockerfile中工作目录一致
              readOnly: true
          volumes:
          - name: conf
            configMap:
              name: {APP_NAME}

    汇总配置

    业务层面

    项目依赖 pom.xml



        org.springframework.boot
        spring-boot-starter-actuator


        io.micrometer
        micrometer-registry-prometheus

    定义访问端口、路径及权限 application.yaml

    spring:
      application:
        name: project-sample
      profiles:
        active: @profileActive@
      lifecycle:
        timeout-per-shutdown-phase: 30s     # 停机过程超时时长设置30s,超过30s,直接停机

    server:
      port: 8080
      shutdown: graceful                    # 默认为IMMEDIATE,表示立即关机;GRACEFUL表示优雅关机

    management:
      server:
        port: 50000                         # 启用独立运维端口
      metrics:
        tags:
          application: ${spring.application.name}
      endpoint:                             # 开启shutdown和health端点
        shutdown:
          enabled: true
        health:
          probes:
            enabled: true
      endpoints:
        web:
          exposure:
            base-path: /actuator            # 指定上下文路径,启用相应端点
            include: health,shutdown,metrics,prometheus

    运维层面

    确保dockerfile模版集成curl工具,否则无法使用curl命令

    FROM openjdk:8-jdk-alpine
    #构建参数
    ARG JAR_FILE
    ARG WORK_PATH="/app"
    ARG EXPOSE_PORT=8080

    #环境变量
    ENV JAVA_OPTS=""
        JAR_FILE=${JAR_FILE}

    #设置时区
    RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && echo 'Asia/Shanghai' >/etc/timezone
    RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories  
        && apk add --no-cache curl
    #将maven目录的jar包拷贝到docker中,并命名为for_docker.jar
    COPY target/$JAR_FILE $WORK_PATH/


    #设置工作目录
    WORKDIR $WORK_PATH


    # 指定于外界交互的端口
    EXPOSE $EXPOSE_PORT
    # 配置容器,使其可执行化
    ENTRYPOINT exec java $JAVA_OPTS -jar $JAR_FILE

    k8s部署模版deployment.yaml

    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: {APP_NAME}
      labels:
        app: {APP_NAME}
    spec:
      selector:
        matchLabels:
          app: {APP_NAME}
      replicas: {REPLICAS}                            # Pod副本数
      strategy:
        type: RollingUpdate                           # 滚动更新策略
        rollingUpdate:
          maxSurge: 1
          maxUnavailable: 0
      template:
        metadata:
          name: {APP_NAME}
          labels:
            app: {APP_NAME}
          annotations:
            timestamp: {TIMESTAMP}
            prometheus.io/port: "50000"               # 不能动态赋值
            prometheus.io/path: /actuator/prometheus
            prometheus.io/scrape: "true"              # 基于pod的服务发现
        spec:
          affinity:                                   # 设置调度策略,采取多主机/多可用区部署
            podAntiAffinity:
              preferredDuringSchedulingIgnoredDuringExecution:
              - weight: 100
                podAffinityTerm:
                  labelSelector:
                    matchExpressions:
                    - key: app
                      operator: In
                      values:
                      - {APP_NAME}
                  topologyKey: "kubernetes.io/hostname" # 多可用区为"topology.kubernetes.io/zone"
          terminationGracePeriodSeconds: 30             # 优雅终止宽限期
          containers:
          - name: {APP_NAME}
            image: {IMAGE_URL}
            imagePullPolicy: Always
            ports:
            - containerPort: {APP_PORT}
            - name: management-port
              containerPort: 50000         # 应用管理端口
            readinessProbe:                # 就绪探针
              httpGet:
                path: /actuator/health/readiness
                port: management-port
              initialDelaySeconds: 30      # 延迟加载时间
              periodSeconds: 10            # 重试时间间隔
              timeoutSeconds: 1            # 超时时间设置
              successThreshold: 1          # 健康阈值
              failureThreshold: 9          # 不健康阈值
            livenessProbe:                 # 存活探针
              httpGet:
                path: /actuator/health/liveness
                port: management-port
              initialDelaySeconds: 30      # 延迟加载时间
              periodSeconds: 10            # 重试时间间隔
              timeoutSeconds: 1            # 超时时间设置
              successThreshold: 1          # 健康阈值
              failureThreshold: 6          # 不健康阈值
            resources:                     # 容器资源管理
              limits:                      # 资源限制(监控使用情况)
                cpu: 0.5
                memory: 1Gi
              requests:                    # 最小可用资源(灵活调度)
                cpu: 0.1
                memory: 200Mi
            env:
              - name: TZ
                value: Asia/Shanghai
    ---
    kind: HorizontalPodAutoscaler            # 弹性伸缩控制器
    apiVersion: autoscaling/v2beta2
    metadata:
      name: {APP_NAME}
    spec:
      scaleTargetRef:
        apiVersion: apps/v1
        kind: Deployment
        name: {APP_NAME}
      minReplicas: {REPLICAS}                # 缩放范围
      maxReplicas: 6
      metrics:
        - type: Resource
          resource:
            name: cpu                        # 指定资源指标
            target:
              type: Utilization
              averageUtilization: 50