Bootstrap

Java常用方法汇总(持续更新)

Java常用应用

20230725更新

1 常用方法

List常用方法

List<Map<String, String>> 根据某个key排序

 List<BusinessProjectVO> records = Optional.ofNullable(businessProjectPage).orElse(new Page<>()).getRecords().stream().peek(e -> {
                R<Long> totalData = remoteSynchronizorService.getTotalData(e.getAppId());
                e.setTotalDate(totalData.getData());
            }).collect(Collectors.toList());
//根据totalDate字段排倒序
records = records.stream().sorted(Comparator.comparing(BusinessProjectVO::getTotalDate).reversed()).collect(Collectors.toList());

List过滤

List<Map<String, Object>> collectDataList = dataList.stream().filter(dataRow -> ("1".equals(String.valueOf(dataRow.get("group_bit"))))).collect(Collectors.toList());

stream方法

//List<Object> 转为 Map<String,Integer>
Map<String,Integer> allBoardNameMap = allDataBoards.stream().collect(Collectors.toMap(dataBoard->dataBoard.getBoardName(), dataBoard -> 1, (existingValue, newValue) -> newValue));

//List<Object> 转为 List<Long>
List<Long> reportIds = boardReportRelations.stream().map(boardReportRelation -> boardReportRelation.getReportId()).collect(Collectors.toList());

List<String> sqlQueryChartIds = boardReports.stream().filter(boardReport -> StringUtils.isNotBlank(boardReport.getChartId())).map(boardReport -> boardReport.getChartId()).collect(Collectors.toList());

判断List集合中是否存在相同元素

//校验是否存在相同资产
List<String> assetNoList = Lists.newArrayList();
beforeMergeAssetList.forEach( asset->{
    String assetNo = asset.getAssetInfo().getAssetNo();
    if (StringUtils.isBlank(assetNo)){throw new JeecgBootException("合并前资产编号不能为空");}
    assetNoList.add(assetNo);
});
Set assetNoSet=new HashSet<>(assetNoList);
if (assetNoList.size() != assetNoSet.size()) {throw new JeecgBootException("请选择不同的资产");}

判空

1 对象
if (Objects.isNull(assetMergeDTO)){ throw new JeecgBootException("请选择资产"); }
2 集合
CollectionUtils.isNotEmpty(records)
3 字符串
StringUtils.isBlank(assetMergeNo)

集合流方法使用

@AutoLog(value = "资产组合-组合日志查询")
    @ApiOperation(value = "资产组合-组合日志查询", notes = "资产组合-组合日志查询")
    @GetMapping("/getMergeLog")
    public Result<?> getMergeLog(@RequestParam(name = "assetNo", required = true) String assetNo) {
        List<AssetMerge> assetMergeList = iAssetMergeService.selectByAssetNo(assetNo);
        Map<String, List<AssetMerge>> listMap = assetMergeList.stream().collect(Collectors.groupingBy(AssetMerge::getAssetMergeNo));
        Set<Map.Entry<String, List<AssetMerge>>> entries = listMap.entrySet();
        JSONArray JSONArray = new JSONArray();
        for (Map.Entry<String, List<AssetMerge>> entry : entries) {
            JSONObject jsonObject = new JSONObject();
            String key = entry.getKey();
            List<AssetMerge> value = entry.getValue();
            List<JSONObject> list = value.stream()
                    .sorted((o1, o2) -> (int) (o1.getId() - o2.getId()))
                    .map(assetSplit -> {
                        JSONObject temp = new JSONObject();
                        temp.put("assetNo", assetSplit.getBeforeMergeAssetNo());
                        temp.put("assetName", assetSplit.getBeforeMergeAssetName());
                        return temp;
                    }).collect(Collectors.toList());
            AssetMerge assetMerge = value.get(0);
            String assetMergeNo = assetMerge.getAssetMergeNo();
            String afterMergeAssetNo = assetMerge.getAfterMergeAssetNo();
            String afterMergeAssetName = assetMerge.getAfterMergeAssetName();
            jsonObject.put("assetMergeNo", assetMergeNo);
            jsonObject.put("afterMergeAssetNo", afterMergeAssetNo);
            jsonObject.put("afterMergeAssetName", afterMergeAssetName);
            jsonObject.put("beforeMergeAssetList", list);
            jsonObject.put("mergeTime", assetMerge.getCreateTime());
            JSONArray.add(jsonObject);
        }
        return Result.ok(JSONArray);
    }

TryCatch基本使用

//参数校验
try {
    if (Objects.isNull(assetMergeDTO)){ throw new JeecgBootException("请选择资产"); }
    List<AssetInfoDTO> beforeMergeAssetList = assetMergeDTO.getBeforeMergeAssetList();
    if (Objects.isNull(beforeMergeAssetList) || beforeMergeAssetList.size() < 2){throw new JeecgBootException("请选择2个及2个以上资产");}
    AssetInfoDTO afterMergeAsset = assetMergeDTO.getAfterMergeAsset();
    if (Objects.isNull(afterMergeAsset)){throw new JeecgBootException("请输入合并后资产信息");}
} catch (JeecgBootException e) {
    e.printStackTrace();
    return Result.error(e.getMessage());
}

导入导出Excel

方法1
@Override
@Transactional(rollbackFor = Exception.class)
public Result<?> importBatchMaintainAssetExcel(HttpServletRequest request) throws IOException {
    MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request;
    Map<String, MultipartFile> fileMap = multipartRequest.getFileMap();
    LoginUser loginUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
    if(loginUser==null){throw new JeecgBootException("登录失效,请重新登录");}
    String username = loginUser.getUsername();
    Date date = new Date();
    // 错误信息
    List<String> errorMessage = new ArrayList<>();
    int successLines = 0, errorLines = 0;
    for (Map.Entry<String, MultipartFile> entity : fileMap.entrySet()) {
        MultipartFile file = entity.getValue();// 获取上传文件对象
        ImportParams params = new ImportParams();
        params.setHeadRows(1);
        params.setTitleRows(2);
        params.setNeedSave(true);
        try {
            List<AssetMaintainExcelImportDTO>  importAssetScraps = ExcelImportUtil.importExcel(file.getInputStream(), AssetMaintainExcelImportDTO.class, params);
            List<AssetMaintainDTO>  list = new ArrayList<>();
            int i=2;
            //先校验参数
            if(CollectionUtils.isEmpty(importAssetScraps)){throw new JeecgBootException("维修信息为空");}
            for (AssetMaintainExcelImportDTO importAssetScrap : importAssetScraps) {
                try {
                    i = i + 1;
                    String assetNo = importAssetScrap.getAssetNo();
                    String remark = importAssetScrap.getRemark();

                    if(StringUtils.isBlank(assetNo)){throw new JeecgBootException("第"+i+"行 资产编号为空");}

                    AssetInfo assetInfo = iAssetInfoService.getByAssetNo(assetNo);
                    if(assetInfo==null){throw new JeecgBootException("第"+i+"行 资产编号错误【"+assetNo+"】");}
                    if(!AssetStatusEnum.INSTORE.getValue().equals(assetInfo.getAssetStatus())){throw new JeecgBootException("第"+i+"行 资产【"+assetNo+"】不是库存状态");}
                    assetInfo.setAssetStatus(AssetStatusEnum.MAINTENANCE.getValue());
                    AssetInfoDTO assetInfoDTO = new AssetInfoDTO();
                    assetInfoDTO.setAssetInfo(assetInfo);
                    iAssetInfoService.assetInfoCheck(assetInfoDTO);
                    AssetMaintainDTO assetScrapDTO = new AssetMaintainDTO();
                    assetScrapDTO.setAssetInfoDTO(assetInfoDTO);
                    assetScrapDTO.setCreateTime(date);
                    assetScrapDTO.setRemark(remark);
                    assetScrapDTO.setCreateBy(username);

                    list.add(assetScrapDTO);
                }catch (JeecgBootException je){
                    throw new JeecgBootException("第"+i+"行数据错误,"+je.getMessage());
                }
            }
            //没有任何问题,再开始保存
            for (AssetMaintainDTO assetScrapDTO : list) {
                try {
                    this.saveMaintainAssetInfo(assetScrapDTO);
                } catch (JeecgBootException je) {
                    errorLines += 1;
                    errorMessage.add(je.getMessage());
                }catch (Exception e) {
                    errorLines += 1;
                    errorMessage.add(e.getMessage());
                }
            }
            successLines+=(importAssetScraps.size()-errorLines);
        }catch (JeecgBootException je){
            return Result.error("文件导入失败:" + je.getMessage());
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            return Result.error("文件导入失败:" + e.getMessage());
        } finally {
            try {
                file.getInputStream().close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    return ImportExcelUtil.imporReturnRes(errorLines,successLines,errorMessage);
}
@AutoLog(value = "资产合并-导出")
@ApiOperation(value = "资产合并-导出", notes = "资产合并-导出")
@RequestMapping(value = "/exportAssetMergeXls")
public ModelAndView exportAssetMergeXls(AssetMergeReq req, HttpServletRequest request) {
    ModelAndView mv = new ModelAndView(new JeecgEntityExcelView());
    List<AssetMergeVO> assetMergeVOList = iAssetMergeService.getAssetMergeList(req);
    //导出文件名称
    mv.addObject(NormalExcelConstants.FILE_NAME, "资产合并列表");
    mv.addObject(NormalExcelConstants.CLASS, AssetMergeVO.class);
    LoginUser user = (LoginUser) SecurityUtils.getSubject().getPrincipal();
    mv.addObject(NormalExcelConstants.PARAMS, new ExportParams("资产合并列表", "导出人:" + user.getRealname(), "导出信息"));
    mv.addObject(NormalExcelConstants.DATA_LIST, assetMergeVOList);
    return mv;
}
方法2

依赖

        <!-- 阿里开源EXCEL -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>easyexcel</artifactId>
            <version>2.2.5</version>
        </dependency>

代码实现

  @PostMapping("/dailyData/export")
  public void exportDailyData(@Valid @RequestBody CustomBoardDailyDataReq req, HttpServletResponse response) {
    try {
      checkDailyDataParams(req);
      List<CustomBoardDailyDataVO> resultList = customBoardDailyDataService.dailyDataExport(req);

      response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
      response.setCharacterEncoding("utf-8");
      String fileName = URLEncoder.encode("日报-"+ DateUtils.getCurrentTimeNoUnderline(), "UTF-8").replaceAll("\\+", "%20");
      response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".csv");
      response.setHeader("file-name", fileName);

      ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream()).build();
//      WriteSheet writeSheet0 = EasyExcel.writerSheet(0, "日报").head(CustomBoardDailyDataVO.class).build();
      //动态隐藏表头
      List<String> columnsList = Lists.newArrayList();
      String columns = req.getColumns();
      if (StringUtils.isNotBlank(columns)){
        columnsList = Arrays.asList(columns.split(","));
      }
      WriteSheet writeSheet0 = EasyExcel.writerSheet(0, "日报")
          .excludeColumnFiledNames(columnsList).head(CustomBoardDailyDataVO.class).build();
      excelWriter.write(resultList, writeSheet0);
      excelWriter.finish();
    }catch (CheckedException ce){
      log.error("error-日报-导出接口:{}", ce);
    } catch (Exception e) {
      log.error("error-日报-导出接口:{}", e);
    }
  }

Json字符串转换为对应类型

//Map、List<String>等等:

//数组String转成List<String>
String s = ["SITE_SET_KANDIAN","SITE_SET_QQ_MUSIC_GAME"]
List<String> parse = (List<String>)JSONArray.parse(siteSet);

//Map的String转换为Map
String s = {"image":"66159322","description":"殿堂级卡牌震撼公测,安卓抢先","video":"157776020","label":[{"type":1,"content":"挂机"},{"type":1,"content":"Q萌卡通"},{"type":1,"content":"二次元"}],"title":"告别假放置!下载领取超多福利","brand":{"brand_name":"冒险世界","brand_img":"66160154"}}
Map map = JSONObject.parseObject(adcreativeElements,Map.class);

后台post请求接口

/**
     * post请求
     *
     * @param url
     * @param jsonObject object
     * @return json object
     */
public static String doPost(String url, JSONObject jsonObject) throws IOException {
    HttpClient client = HttpClientBuilder.create().build();
    HttpPost post = new HttpPost(url);
    String result =  null;

    RequestConfig requestConfig = RequestConfig.custom()
        .setConnectTimeout(10000)
        .setConnectionRequestTimeout(10000)
        .setSocketTimeout(60000)
        .build();
    post.setConfig(requestConfig);
    post.addHeader("Content-type","application/json; charset=utf-8");
    post.setHeader("Accept", "application/json");
    StringEntity s = new StringEntity(jsonObject.toString(),"UTF-8");
    post.setEntity(s);
    HttpResponse res = client.execute(post);
    if (res.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
        HttpEntity entity = res.getEntity();
        result = EntityUtils.toString(entity);
    }
    return result;
}
/*
     * 测试 用户标签创建方式为 首末次特征
     * */
public static void main(String[] args) throws Exception{
    String url = "http://localhost:9201/useranalysis/saveUserLabelMeta";
    JSONObject json = new JSONObject();
    json.put("appId",1386652408108683264L);
    json.put("createMode",3);//首末次特征
    json.put("dataUpdate",1);
    json.put("conditionJson","{\"columnName\":\"#screen_width\",\"columnType\":\"DOUBLE\",\"eventName\":\"login\",\"filts\":[{\"columnName\":\"#account_id\",\"comparator\":\"notEqual\",\"ftv\":[\"444444\"],\"selectType\":\"STRING\",\"tableType\":\"user\"}],\"isFirstEvent\":true,\"isUserFilter\":true,\"recentDay\":\"0-7\",\"relation\":\"or\"}");
    json.put("labelName","标签_20210429_1002");
    json.put("labelNo","tag_20210429_2");
    json.put("userId",101);
    System.out.println(json.toString());
    String s = HttpUtil.doPost(url, json);
    System.out.println(s);
}

@Value注解读取配置属性值

assetInfo:
  deviceTypeID: "1402108939024203778"
@Value("${assetInfo.deviceTypeID}")
private String DEVICE_TYPE_ID;

正则匹配

package org.jeecg.modules.base.util;

import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * @description: 正则表达式工具类
 * @author: nml
 * @time: 2020/12/23 11:56
 **/
public final class RegexUtils {

	/**
	 * 获得匹配正则表达式的内容
	 * @param str 字符串
	 * @param reg 正则表达式
	 * @param isCaseInsensitive 是否忽略大小写,true忽略大小写,false大小写敏感
	 * @return 匹配正则表达式的字符串,组成的List
	 */
	public static List<String> getMatchList(final String str, final String reg, final boolean isCaseInsensitive) {
		ArrayList<String> result = new ArrayList<String>();
		Pattern pattern = null;
		if (isCaseInsensitive) {
			//编译正则表达式,忽略大小写
			pattern = Pattern.compile(reg, Pattern.CASE_INSENSITIVE);
		} else {
			//编译正则表达式,大小写敏感
			pattern = Pattern.compile(reg);
		}
		Matcher matcher = pattern.matcher(str);// 指定要匹配的字符串
		while (matcher.find()) { //此处find()每次被调用后,会偏移到下一个匹配
			result.add(matcher.group());//获取当前匹配的值
		}
		result.trimToSize();
		return result;
	}

	/**
	 * 获取第一个匹配正则表达式的子串
	 * @param str 完整字符串
	 * @param reg 正则表达式
	 * @param isCaseInsensitive 是否忽略大小写,true表示忽略,false表示大小写敏感。
	 * @return 第一个匹配正则表达式的子串。
	 */
	public static String getFirstMatch(final String str, final String reg, final boolean isCaseInsensitive) {
		Pattern pattern = null;
		if (isCaseInsensitive) {
			//编译正则表达式,忽略大小写
			pattern = Pattern.compile(reg, Pattern.CASE_INSENSITIVE);
		} else {
			//编译正则表达式,大小写敏感
			pattern = Pattern.compile(reg);
		}
		Matcher matcher = pattern.matcher(str);// 指定要匹配的字符串
		if (matcher.find()) {
			return matcher.group();
		}
		return null;
	}

    //匹配:张三[101]。其中汉字2-4个,数字3-10个。
	public static boolean checkNameAndWorkNo(String dataStr) {
		String regFormat = "^[\\u4E00-\\u9FA5]{2,4}\\[\\d{3,10}\\]$";
		boolean isMatch = Pattern.matches(regFormat , dataStr);
		return isMatch;
	}


}

反射机制

Java反射机制是Java语言的一个特性,它可以在运行时获取类的结构信息,如类名、方法名、属性名等,并且可以动态地创建对象、调用方法、访问属性等。Java反射机制为程序员提供了更大的灵活性和编程能力,通常用于以下几个方面:

  1. 动态加载类:Java反射机制可以在程序运行时根据类名称加载相应的类,实现类的动态加载。
  2. 动态创建对象:Java反射机制可以通过调用Class对象的newInstance()方法动态地创建对象,有利于代码的扩展性和复用性。
  3. 动态调用方法:Java反射机制可以在运行时动态调用方法,不需要事先知道方法名,极大地拓展了程序的功能性。
  4. 访问私有属性和方法:Java反射机制可以通过设置setAccessible()方法来访问类的私有属性和方法,而不受访问修饰符的限制。
  5. 实现泛型数组:Java反射机制可以实现泛型数组的创建和访问,提高了程序的可读性和可维护性。

总之,Java反射机制是Java语言中的一个重要机制,通过反射技术,程序员可以在运行时获取并操作类的结构信息,提高程序的灵活性和可扩展性。

1 获得Class:主要有三种方法:

(1)Object–>getClass

(2)任何数据类型(包括基本的数据类型)都有一个“静态”的class属性

(3)通过class类的静态方法:forName(String className)(最常用)

package fanshe;
 
public class Fanshe {
	public static void main(String[] args) {
		//第一种方式获取Class对象  
		Student stu1 = new Student();//这一new 产生一个Student对象,一个Class对象。
		Class stuClass = stu1.getClass();//获取Class对象
		System.out.println(stuClass.getName());
		
		//第二种方式获取Class对象
		Class stuClass2 = Student.class;
		System.out.println(stuClass == stuClass2);//判断第一种方式获取的Class对象和第二种方式获取的是否是同一个
		
		//第三种方式获取Class对象
		try {
			Class stuClass3 = Class.forName("fanshe.Student");//注意此字符串必须是真实路径,就是带包名的类路径,包名.类名
			System.out.println(stuClass3 == stuClass2);//判断三种方式是否获取的是同一个Class对象
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		}
		
	}
}

注意,在运行期间,一个类,只有一个Class对象产生,所以打印结果都是true;

三种方式中,常用第三种,第一种对象都有了还要反射干什么,第二种需要导入类包,依赖太强,不导包就抛编译错误。一般都使用第三种,一个字符串可以传入也可以写在配置文件中等多种方法。

测试类

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
package com.idongyou.dongxin.thirdparty;

import com.alibaba.fastjson.JSON;
import com.idongyou.dongxin.thirdparty.dao.doris.CustomerAccountRoleInfoDao;
import com.idongyou.dongxin.thirdparty.model.CustomerAccountRoleInfo;
import com.idongyou.dongxin.thirdparty.model.dto.CustomerAccountRoleInfoDTO;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@Slf4j
@SpringBootTest
public class CustomerTest {

  @Autowired
  private CustomerAccountRoleInfoDao customerAccountRoleInfoDao;

  @Test
  public void testQueryOneByRole(){
    CustomerAccountRoleInfoDTO req = new CustomerAccountRoleInfoDTO();
    req.setAppMain(16);
    CustomerAccountRoleInfo customerAccountRoleInfo = customerAccountRoleInfoDao.queryOneByRole(req);
    log.info(JSON.toJSONString(customerAccountRoleInfo));
  }

}

多线程ThreadPoolExecutor

    //初始化一个线程池
protected final ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(Runtime.getRuntime().availableProcessors(),
            Runtime.getRuntime().availableProcessors(),
            0L,
            TimeUnit.SECONDS,
            new LinkedBlockingDeque<>(1024)
    );
    
    //查询时,多线程查询
        public ListResult<GameRolePayDTO> gameRolePayResult(RealTimeReq req) {
        //
        ListResult<GameRolePayDTO> result = new ListResult<>();

        CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
            // 列表
            result.setList(gameRolePayList(req));
        }, threadPoolExecutor).thenRunAsync(() -> {
            // 汇总
            result.setSummary(gameRolePaySummary(req));
        }, threadPoolExecutor);
        try {
            future.get(30,TimeUnit.SECONDS);
        } catch (Exception e) {
            log.error("查询游戏角色付费失败..., {}",e);
        }
        return result;
    }

设计模式之单例模式

Java中的单例模式是一种常用的设计模式,它保证一个类在整个应用程序中只有一个实例,并提供一个全局访问点。

Java实现单例模式的方式有多种,其中比较常用的方法如下:

1.懒汉式:在首次使用时创建实例。代码如下:

Copy Codepublic class Singleton {
    private static Singleton instance = null;
    private Singleton() {}
    
    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

2.饿汉式:在类加载时创建实例。代码如下:

Copy Codepublic class Singleton {
    private static final Singleton instance = new Singleton();
    private Singleton() {}
    
    public static Singleton getInstance() {
        return instance;
    }
}

3.双重校验锁:在多线程环境中保证单例实例的线程安全创建。代码如下:

Copy Codepublic class Singleton {
    private static volatile Singleton instance = null;
    private Singleton() {}
    
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

4.静态内部类:利用类加载器机制实现线程安全的延迟初始化。代码如下:

Copy Codepublic class Singleton {
    private Singleton() {}
    
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
    
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

以上是常用的几种单例模式实现方式,开发人员可以根据具体场景选择适合自己的方式来实现单例模式,以保证系统的性能和安全性。

不可变map

Java中可以通过使Map的键和值都是不可变类型来实现不变Map。其中,可以使用Collections.unmodifiableMap()方法创建一个只读的Map视图,或者使用Guava中的ImmutableMap来创建一个完全不可变的Map。

以下是两种实现方式的示例代码:

  1. 使用Collections.unmodifiableMap()方法创建只读的Map视图
Copy CodeMap<String, String> originalMap = new HashMap<>();
// 添加元素到原始Map
originalMap.put("key1", "value1");
originalMap.put("key2", "value2");

// 创建只读Map视图
Map<String, String> immutableMap = Collections.unmodifiableMap(originalMap);

// 试图修改只读Map视图的内容会抛出UnsupportedOperationException异常
immutableMap.put("key3", "value3"); // UnsupportedOperationException
  1. 使用Guava中的ImmutableMap创建完全不可变的Map
Copy Code// 创建一个不可变Map
Map<String, String> immutableMap = ImmutableMap.of(
    "key1", "value1",
    "key2", "value2"
);

// 试图修改不可变Map的内容会抛出UnsupportedOperationException异常
immutableMap.put("key3", "value3"); // UnsupportedOperationException

以上两种方式都可以用来创建不变Map,但它们有一些差异。使用Collections.unmodifiableMap()方法创建的只读Map仍然可以通过原始Map来修改,而使用Guava中的ImmutableMap创建的完全不可变Map则没有这个问题。

ObjectMapper

ObjectMapper 类提供了一系列方法用于实现 Java 对象与 JSON 数据之间的转换。以下是一些常用的 ObjectMapper 方法:

  1. writeValueAsString(Object value):将指定的 Java 对象转换为 JSON 字符串。
ObjectMapper objectMapper = new ObjectMapper();
String jsonString = objectMapper.writeValueAsString(myObject);
  1. writeValue(File file, Object value):将指定的 Java 对象转换为 JSON,并写入到文件中。
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.writeValue(new File("data.json"), myObject);
  1. readValue(String content, Class<T> valueType):将 JSON 字符串转换为指定类型的 Java 对象。
ObjectMapper objectMapper = new ObjectMapper();
MyObject myObject = objectMapper.readValue(jsonString, MyObject.class);
  1. readValue(File file, Class<T> valueType):从文件中读取 JSON 数据,并将其转换为指定类型的 Java 对象。
ObjectMapper objectMapper = new ObjectMapper();
MyObject myObject = objectMapper.readValue(new File("data.json"), MyObject.class);
  1. convertValue(Object fromValue, Class<T> toValueType):将一个对象转换为指定类型的对象。
ObjectMapper objectMapper = new ObjectMapper();
MyObject myObject = objectMapper.convertValue(map, MyObject.class);
  1. setPropertyNamingStrategy(PropertyNamingStrategy naming):设置命名策略,用于在序列化和反序列化过程中控制属性命名的转换方式。
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE);
  1. setSerializationInclusion(JsonInclude.Include include):设置对象属性的序列化包含规则,用于控制哪些属性会被包含在生成的 JSON 中。
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);

这只是一小部分 ObjectMapper 提供的方法示例。ObjectMapper 还提供了许多其他方法,用于更高级的定制和操作。你可以根据具体需求查阅 ObjectMapper 类的官方文档以获取更详细的信息。

应用实例:

  // 保存数据JSON文件到指定目录
  public static void saveDataToFile(Object data,String fileStoragePath) throws Exception{
    ObjectMapper objectMapper = new ObjectMapper();
//    String dataJson = objectMapper.writeValueAsString(data);
    File file = new File(fileStoragePath);
    if (file.exists() && file.isFile()) {
      // 文件存在
      log.info("saveDataToFile:{}","文件存在,覆盖保存");
      objectMapper.writeValue(file,data);
    } else {
      // 文件不存在
      //直接保存
      objectMapper.writeValue(file,data);
    }
  }

Java生成唯一id

在 Java 中,可以使用以下几种方式生成唯一ID:

  1. UUID(Universally Unique Identifier):UUID 是一种由标准化的 128 位数字(32 个十六进制数)组成的唯一标识符。Java 提供了 java.util.UUID 类来生成 UUID。

    import java.util.UUID;
    
    // 生成随机的 UUID
    UUID uuid = UUID.randomUUID();
    String uniqueId = uuid.toString();
    
  2. Snowflake 算法:Snowflake 是一种分布式唯一 ID 生成算法,通过结合时间戳、机器ID和序列号生成唯一的分布式ID。你可以借助一些第三方库或自行实现 Snowflake 算法。

  3. 数据库自增列:在关系型数据库中,可以使用自增列来生成唯一的ID。在插入数据时,数据库会自动为该列生成一个唯一的递增值。

  4. 第三方库:还有一些第三方库如 Twitter 的 Snowflake、Pinterest 的 IdGenerator 等,它们提供了更灵活的唯一 ID 生成方案。

选择哪种方式取决于你的具体需求和应用场景。如果只需要生成唯一的标识符,UUID 是一个简单且常用的选择。如果需要生成全局唯一且趋势递增的 ID,并且要求高性能和可扩展性,则 Snowflake 算法可能更适合。

希望以上信息对你有所帮助。如果你有任何其他问题,请随时提问。

File工具类

Java中的File流是一种用于读取和写入文件数据的流。它提供了一种简单而灵活的方式来进行文件的输入和输出操作。

Java中的File类表示文件或目录的抽象路径名,在使用File流时通常与InputStream(输入流)和OutputStream(输出流)结合使用,来实现对文件的读取和写入操作。

下面是使用FileInputStream和FileOutputStream读写文件的简单示例:

  1. 使用FileInputStream读取文件:
try {
    File file = new File("path/to/file.txt");
    FileInputStream fis = new FileInputStream(file);
    
    int data;
    while ((data = fis.read()) != -1) {
        // 处理读取到的数据
        System.out.print((char) data);
    }
    
    fis.close();
} catch (IOException e) {
    e.printStackTrace();
}

上述代码中,通过创建FileInputStream对象并传入要读取的文件路径,然后使用read()方法逐个字节地读取文件内容,直到读取结束。

  1. 使用FileOutputStream写入文件:
try {
    File file = new File("path/to/file.txt");
    FileOutputStream fos = new FileOutputStream(file);
    
    String content = "Hello, World!";
    fos.write(content.getBytes());
    
    fos.close();
} catch (IOException e) {
    e.printStackTrace();
}

上述代码中,通过创建FileOutputStream对象并传入要写入的文件路径,然后使用write()方法将字符串内容转换为字节数组并写入文件中。

需要注意的是,在使用File流进行文件读写操作时,需要适当处理异常并在操作完成后关闭流,以确保资源被正确释放。

除了FileInputStream和FileOutputStream,Java还提供了其他类型的File流,如BufferedInputStream、BufferedOutputStream,它们可以提供更高效的读写方式。

总结来说,Java的File流是一种用于读取和写入文件数据的功能强大的工具,在文件操作中起着重要的作用。通过File流,我们可以轻松地对文件进行读写,并根据需要进行处理和操作。

应用实例:

package com.idongyou.dongxin.system.util;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.gson.JsonParser;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;

@Slf4j
public class FileUtils {

  public static boolean isJsonFile(MultipartFile file) {
    try {
      String content = new String(file.getBytes());
      JsonParser parser = new JsonParser();
      parser.parse(content); // 尝试解析文件内容
      return true; // 格式正确
    } catch (Exception e) {
      log.error("error:",e);
      return false; // 格式错误
    }
  }

  // 保存文件到指定目录
  public static void saveMultipartFile(MultipartFile file, String fileDir) throws IOException {
    // 获取文件名
    String fileName = file.getOriginalFilename();
    // 在指定目录下创建一个空文件
    Path filePath = Path.of(fileDir, fileName);
    //如果文件不存在,就创建
    if (!Files.exists(filePath)){
      Files.createFile(filePath);
    }
    // 将上传的文件内容保存到指定目录下的文件中
    Files.copy(file.getInputStream(), filePath, StandardCopyOption.REPLACE_EXISTING);
  }

  // 保存数据JSON文件到指定目录
  public static void saveDataToFile(Object data,String fileStoragePath) throws Exception{
    ObjectMapper objectMapper = new ObjectMapper();
//    String dataJson = objectMapper.writeValueAsString(data);
    File file = new File(fileStoragePath);
    if (file.exists() && file.isFile()) {
      // 文件存在
      log.info("saveDataToFile:{}","文件存在,覆盖保存");
      objectMapper.writeValue(file,data);
    } else {
      // 文件不存在
      //直接保存
      objectMapper.writeValue(file,data);
    }
  }

}

时间工具类

在Java中,SimpleDateFormat是一个用于格式化和解析日期时间的类。它允许我们将日期时间对象转换为特定格式的字符串,以及将特定格式的字符串转换为日期时间对象。

下面是使用SimpleDateFormat进行日期时间格式化和解析的示例:

  1. 将日期时间对象格式化为字符串:
import java.text.SimpleDateFormat;
import java.util.Date;

SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

Date now = new Date();
String formattedDate = sdf.format(now);

System.out.println(formattedDate); // 输出:2023-07-21 02:10:03

上述代码中,首先创建了一个SimpleDateFormat对象,并指定了日期时间的格式模式(“yyyy-MM-dd HH:mm:ss”)。然后,使用format()方法将当前的Date对象按照指定的格式模式转换为字符串。

  1. 将字符串解析为日期时间对象:
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

String dateString = "2023-07-21 02:10:03";
try {
    Date parsedDate = sdf.parse(dateString);
    System.out.println(parsedDate); // 输出:Thu Jul 21 02:10:03 GMT 2023
} catch (ParseException e) {
    e.printStackTrace();
}

上述代码中,首先创建了一个SimpleDateFormat对象,并指定了日期时间的格式模式(“yyyy-MM-dd HH:mm:ss”)。然后,使用parse()方法将字符串按照指定的格式模式解析为Date对象。

需要注意的是,在使用SimpleDateFormat进行日期时间格式化和解析时,格式模式的字符串必须与要处理的日期时间字符串保持一致,否则可能会抛出ParseException异常。

除了常见的日期时间格式模式(如"yyyy-MM-dd HH:mm:ss"),SimpleDateFormat还支持其他模式,如年份(“yyyy”)、月份(“MM”)、日(“dd”)、小时(“HH”)、分钟(“mm”)等,具体可参考Java文档中SimpleDateFormat的相关信息。

总结来说,SimpleDateFormat是Java中用于日期时间格式化和解析的类。通过它,我们可以将日期时间对象转换为特定格式的字符串,以及将特定格式的字符串转换为日期时间对象,非常方便实用。

应用实例:

package com.idongyou.dongxin.system.util;

import cn.hutool.core.date.DateUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import java.beans.PropertyEditorSupport;
import java.sql.Timestamp;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.TimeZone;

/**
 * DateUtils
 *
 * @author nml
 * @date 2021/7/21 14:58
 */
@Slf4j
public class DateUtils extends PropertyEditorSupport {

    public static ThreadLocal<SimpleDateFormat> date_sdf = new

            ThreadLocal<SimpleDateFormat>() {

                @Override
                protected SimpleDateFormat initialValue() {
                    return new SimpleDateFormat("yyyy-MM-dd");
                }


            };
    public static ThreadLocal<SimpleDateFormat> datetimeFormat = new

            ThreadLocal<SimpleDateFormat>() {

                @Override
                protected SimpleDateFormat initialValue() {
                    return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
                }


            };
    // 以毫秒表示的时间
    private static final long DAY_IN_MILLIS = 24 * 3600 * 1000;
    private static final long HOUR_IN_MILLIS = 3600 * 1000;
    private static final long MINUTE_IN_MILLIS = 60 * 1000;
    private static final long SECOND_IN_MILLIS = 1000;
    public static final String YYYY_MM_DD = "yyyy-MM-dd";
    public static final String YYYY_MM_DD_HH_MM = "yyyy-MM-dd HH:mm";
    public static final String YYYY_MM_DD_HH_MM_SS = "yyyy-MM-dd HH:mm:ss";
    public static final String YYYYMMDD = "yyyyMMdd";
    public static final String YYMMDD = "yyMMdd";
    public static final String YYMMDDHHMMSS = "yyMMddHHmmss";
    public static final String YYYYMMDDHHMMSS = "yyyyMMddHHmmss";
    public static final String YYMMDDHHMMSSSSS = "yyMMddHHmmssSSS";
    public static final String YYYYMMDD_HH_MM_SS = "yyyy_MM_dd_HH_mm_ss";

    // 指定模式的时间格式
    private static SimpleDateFormat getSDFormat(String pattern) {
        return new SimpleDateFormat(pattern);
    }


    /**
     * 当前日历,这里用中国时间表示
     *
     * @return 以当地时区表示的系统当前日历
     */
    public static Calendar getCalendar() {
        return Calendar.getInstance();
    }
    
        /**
     * 获取时间字符串
     */
    public static String getDataString(SimpleDateFormat formatstr) {
        return formatstr.format(getCalendar().getTime());
    }


    /**
     * 日期字符串 转为 Calendar
     */
    public static Calendar dateStr2Calendar(String date, String pattern) {
        SimpleDateFormat format = getSDFormat(pattern);
        Calendar calendar = Calendar.getInstance();
        try {
            calendar.setTime(format.parse(date));
        } catch (ParseException e) {
            e.printStackTrace();
        }
        return calendar;
    }


    /**
     * 指定毫秒数表示的日历
     *
     * @param millis 毫秒数
     * @return 指定毫秒数表示的日历
     */
    public static Calendar getCalendar(long millis) {
        Calendar cal = Calendar.getInstance();
        cal.setTime(new Date(millis));
        return cal;
    }

    public static DateFormat getDateFormat(String dateFormat) {
        return new SimpleDateFormat(dateFormat);
    }


    /**
     * 取得某天所在周的第一天
     *
     * @param date
     * @return
     */
    public static Date getFirstDayOfWeek(Date date) {
        Calendar c = new GregorianCalendar();
        c.setFirstDayOfWeek(Calendar.MONDAY);
        c.setTime(date);
        c.set(Calendar.DAY_OF_WEEK, c.getFirstDayOfWeek());
        return c.getTime();
    }


    /**
     * 获取星期几
     *
     * @param date
     * @return
     */
    public static int getWeekDay(Date date) {
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(date);
        int dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK);
        return dayOfWeek - 1;
    }


    /**
     * 根据开始时间和结束时间返回时间段内的时间集合
     *
     * @param beginDateStr
     * @param endDateStr
     * @return List
     */
    public static List<String> getDateStrsBetweenTwoDate(String beginDateStr, String endDateStr) {
        List<String> lDate = new ArrayList<String>();
        lDate.add(beginDateStr);// 把开始时间加入集合
        Calendar cal = Calendar.getInstance();
        // 使用给定的 Date 设置此 Calendar 的时间
        Date beginDate = str2Date(beginDateStr, date_sdf.get());
        Date endDate = str2Date(endDateStr, date_sdf.get());
        cal.setTime(beginDate);
        boolean bContinue = true;
        while (bContinue) {
            // 根据日历的规则,为给定的日历字段添加或减去指定的时间量
            cal.add(Calendar.DAY_OF_MONTH, 1);
            // 测试此日期是否在指定日期之后
            if (endDate.after(cal.getTime())) {
                lDate.add(date2Str(cal.getTime(), date_sdf.get()));
            } else {
                break;
            }
        }
        lDate.add(endDateStr);// 把结束时间加入集合
        return lDate;
    }


    /**
     * 根据开始时间和结束时间返回时间段内的时间集合
     *
     * @param beginDate
     * @param endDate
     * @return List
     */
    public static List<Date> getDatesBetweenTwoDate(Date beginDate, Date endDate) {
        List<Date> lDate = new ArrayList<Date>();
        lDate.add(beginDate);// 把开始时间加入集合
        Calendar cal = Calendar.getInstance();
        // 使用给定的 Date 设置此 Calendar 的时间
        cal.setTime(beginDate);
        boolean bContinue = true;
        while (bContinue) {
            // 根据日历的规则,为给定的日历字段添加或减去指定的时间量
            cal.add(Calendar.DAY_OF_MONTH, 1);
            // 测试此日期是否在指定日期之后
            if (endDate.after(cal.getTime())) {
                lDate.add(cal.getTime());
            } else {
                break;
            }
        }
        lDate.add(endDate);// 把结束时间加入集合
        return lDate;
    }
    
    
}    

2 框架整合应用

MybatisPlus基本使用

//实体类
@Data
@TableName("asset_merge")
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = false)
@ApiModel(value="资产合并记录表", description="资产合并记录表")
public class AssetMerge implements Serializable {

  private static final long serialVersionUID = 1L;

  /**自增ID*/
  @Excel(name = "ID", width = 15)
  @ApiModelProperty(value = "ID")
  @TableId(type = IdType.AUTO)
  private Long id;
}

@Data
public class AssetMergeReq extends Page {
  /**合并编号*/
  private String assetMergeNo;
  /**合并后资产编号*/
  private String afterMergeAssetNo;
}

//Mapper
public interface AssetMergeMapper extends BaseMapper<AssetMerge> {
  IPage<AssetMergeVO> getAssetMergePage(AssetMergeReq req);
  List<AssetMerge> selectByAssetNo(String assetNo);
}

//Service
public interface IAssetMergeService extends IService<AssetMerge> {
...
}

@Service
@Slf4j
public class AssetMergeServiceImpl extends ServiceImpl<AssetMergeMapper, AssetMerge> implements IAssetMergeService {
    ...
}

//Controller
@AutoLog(value = "资产合并-导出")
@ApiOperation(value = "资产合并-导出", notes = "资产合并-导出")
@RequestMapping(value = "/exportAssetMergeXls")
public ModelAndView exportAssetMergeXls(AssetMergeReq req, HttpServletRequest request) {
    ModelAndView mv = new ModelAndView(new JeecgEntityExcelView());
    List<AssetMergeVO> assetMergeVOList = iAssetMergeService.getAssetMergeList(req);
    //导出文件名称
    mv.addObject(NormalExcelConstants.FILE_NAME, "资产合并列表");
    mv.addObject(NormalExcelConstants.CLASS, AssetMergeVO.class);
    LoginUser user = (LoginUser) SecurityUtils.getSubject().getPrincipal();
    mv.addObject(NormalExcelConstants.PARAMS, new ExportParams("资产合并列表", "导出人:" + user.getRealname(), "导出信息"));
    mv.addObject(NormalExcelConstants.DATA_LIST, assetMergeVOList);
    return mv;
}

应用实例:

package com.idongyou.dongxin.system.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.google.common.collect.ImmutableMap;
import com.idongyou.dongxin.common.core.constant.Constants;
import com.idongyou.dongxin.common.core.exception.BusinessException;
import com.idongyou.dongxin.common.security.utils.SecurityUtils;
import com.idongyou.dongxin.system.domain.SysImportExportFileInfo;
import com.idongyou.dongxin.system.domain.req.SysImportExportFileInfoReq;
import com.idongyou.dongxin.system.mapper.SysImportExportFileInfoMapper;
import com.idongyou.dongxin.system.rebuild.enums.ReturnCodeEnum;
import com.idongyou.dongxin.system.service.SysImportExportFileInfoService;
import com.idongyou.dongxin.system.util.DateUtils;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Service;
import java.util.*;

@Service
public class SysImportExportFileInfoServiceImpl extends ServiceImpl<SysImportExportFileInfoMapper, SysImportExportFileInfo> implements SysImportExportFileInfoService {

  //字段名称映射关系的不变Map
  private static final Map<String, String> FILED_IMMUTABLE_MAP = ImmutableMap.<String, String>builder()
      .put("createTime", "create_time")
      .put("fileId", "file_id")
      .build();

  @Override
  public IPage<SysImportExportFileInfo> queryPage(SysImportExportFileInfoReq req) {
    long current = req.getCurrent();
    long size = req.getSize();
    Integer userId = SecurityUtils.getUserId();
    long startTime = req.getStartTime();
    long endTime = req.getEndTime();
    //分页
    Page<SysImportExportFileInfo> page = new Page<>(current, size);
    //筛选、排序
    QueryWrapper<SysImportExportFileInfo> queryWrapper = new QueryWrapper<>();
    //筛选
    queryWrapper.eq("del_flag", Constants.DEL_FLAG_0);
    queryWrapper.eq("app_id",req.getAppId());
    queryWrapper.eq("user_id",userId);
    queryWrapper.ge("create_time",DateUtils.getDateByLongMillis(startTime));
    queryWrapper.le("create_time",DateUtils.getDateByLongMillis(endTime));
    String fileName = req.getFileName();
    if (StringUtils.isNotBlank(fileName)){
      queryWrapper.like("file_name",fileName);
    }
    Integer status = req.getStatus();
    if (Objects.nonNull(status)){
      queryWrapper.eq("status",status);
    }
    Integer getOperationType = req.getOperationType();
    if (Objects.nonNull(getOperationType)){
      queryWrapper.eq("operation_type",getOperationType);
    }
    //排序
    String sortColumn = req.getSortColumn();
    String sortType = req.getSortType();
    Boolean isAsc = Boolean.FALSE;
    if (StringUtils.isBlank(sortColumn)){
      sortColumn = "fileId";
    }
    if (StringUtils.isNotBlank(sortType)){
      if ("asc".equalsIgnoreCase(sortType)){
        isAsc = Boolean.TRUE;
      }
    }
    queryWrapper.orderBy(Boolean.TRUE,isAsc,FILED_IMMUTABLE_MAP.get(sortColumn));
    //查询
    return this.page(page, queryWrapper);
  }

  @Override
  public boolean saveOne(SysImportExportFileInfo info) {
    if (Objects.isNull(info)){
      throw new BusinessException(ReturnCodeEnum.FILE_INFO_SAVE_PARAMS_IS_NULL);
    }
    return this.save(info);
  }

  @Override
  public boolean deleteBatch(List<Long> ids) {
    boolean isDelete = this.update(Wrappers.<SysImportExportFileInfo>lambdaUpdate()
        .eq(SysImportExportFileInfo::getDelFlag, Constants.DEL_FLAG_0)
        .in(SysImportExportFileInfo::getFileId, ids)
        .set(SysImportExportFileInfo::getDelFlag, Constants.DEL_FLAG_1)
    );
    return isDelete;
  }

}

SpringBoot服务替换tomcat

使用undertow

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <exclusions>
        <exclusion>
            <artifactId>spring-boot-starter-tomcat</artifactId>
            <groupId>org.springframework.boot</groupId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-undertow</artifactId>
</dependency>
undertow:
  # HTTP post内容的最大大小。当值为-1时,默认值为大小是无限的
  max-http-post-size: -1
  # 以下的配置会影响buffer,这些buffer会用于服务器连接的IO操作,有点类似netty的池化内存管理
  # 每块buffer的空间大小,越小的空间被利用越充分
  buffer-size: 512
  # 是否分配的直接内存
  direct-buffers: true
  threads:
    # 设置IO线程数, 它主要执行非阻塞的任务,它们会负责多个连接, 默认设置每个CPU核心一个线程
    io: 8
    # 阻塞任务线程池, 当执行类似servlet请求阻塞操作, undertow会从这个线程池中取得线程,它的值设置取决于系统的负载
    worker: 256

SpringBoot数据源配置

MybatisPlusConfig
@Configuration
public class MybatisPlusConfig {
	/**
	 * 初始化MybatisPlusInterceptor
	 * */
	@Bean
	public MybatisPlusInterceptor mybatisPlusInterceptor() {
		MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
		interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
		return interceptor;
	}
}
DorisDataSourceConfig
@Configuration
//扫描dao层代码的包路径
@MapperScan(basePackages = "com.idongyou.dongxin.thirdparty.dao.doris",sqlSessionFactoryRef = "dorisSqlSessionFactory")
public class DorisDataSourceConfig {

	@Autowired
	private MybatisPlusInterceptor mybatisPlusInterceptor;

	@Bean(name = "dorisTemplate")
	public JdbcTemplate dorisJdbcTemplate(@Qualifier("dorisDataSource") DataSource dataSource) {
		return new JdbcTemplate(dataSource);
	}

	@Bean(name = "dorisDataSource")
    //读取nacos配置文件中的数据源值
	@ConfigurationProperties(prefix ="spring.datasource.druid.doris")
	public DataSource dorisDataSource() {
		return DataSourceBuilder.create().build();
	}

	@Bean(name = "dorisSqlSessionFactory")
	// 表示这个数据源是默认数据源
	// @Qualifier表示查找对象
	public MybatisSqlSessionFactoryBean cstSqlSessionFactory(@Qualifier("dorisDataSource") DataSource datasource) throws Exception {
		MybatisSqlSessionFactoryBean sqlSessionFactoryBean = new MybatisSqlSessionFactoryBean();
		sqlSessionFactoryBean.setDataSource(dorisDataSource());
		//这里如果用mybatis plus的话,要用mybatis-plus的configuration
		MybatisConfiguration configuration = new MybatisConfiguration();
		//configuration.setMapUnderscoreToCamelCase(false); 
		sqlSessionFactoryBean.setConfiguration(configuration);
        //mapper.xml文件的路径
		sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:/mapper/doris/**/*.xml"));

		//设置 MyBatis-Plus 分页插件
		Interceptor[] plugins = {mybatisPlusInterceptor};
		sqlSessionFactoryBean.setPlugins(plugins);
		return sqlSessionFactoryBean;
	}

	@Bean("dorisSqlSessionTemplate")
	public SqlSessionTemplate test1sqlsessiontemplate(
			@Qualifier("dorisSqlSessionFactory") SqlSessionFactory sessionfactory) {
		return new SqlSessionTemplate(sessionfactory);
	}
	
}
Nacos.yml

dev

spring:
  # 应用名称
  application:
    name: dongxin-third-party-invoking-dev
    

  # 数据源
  datasource: 
    druid: 
      #doris数据源
      doris: 
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql:loadbalance://ip:9030/dc_dwd?autoReconnect=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false
        username: dev
        password: dev
        type: com.alibaba.druid.pool.DruidDataSource
        initial-size: 3
        min-idle: 3
        max-wait-millis: 60000
        timeBetweenEvictionRunsMillis: 60000
        # 配置一个连接在池中最小生存的时间,单位是毫秒
        minEvictableIdleTimeMillis: 300000
        validationQuery: SELECT 1
        testWhileIdle: true
        testOnBorrow: true
        testOnReturn: false
        maxPoolPreparedStatementPerConnectionSize: 20
        # 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
        filters: stat,wall
        # 通过connectProperties属性来打开mergeSql功能;慢SQL记录
        connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000

prod

mysql8版本:数据源

spring:
  # 应用名称
  application:
    name: dongxin-third-party-invoking-dev
    
  # 数据源
  datasource: 
    druid: 
      #doris数据源
      doris: 
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql:loadbalance://ip:port,ip:port,ip:port/dc_dwd?autoReconnect=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false
        username: xxx
        password: xxx
        type: com.alibaba.druid.pool.DruidDataSource
        initial-size: 3
        min-idle: 3
        max-wait-millis: 60000
        timeBetweenEvictionRunsMillis: 60000
        # 配置一个连接在池中最小生存的时间,单位是毫秒
        minEvictableIdleTimeMillis: 300000
        validationQuery: SELECT 1
        testWhileIdle: true
        testOnBorrow: true
        testOnReturn: false
        maxPoolPreparedStatementPerConnectionSize: 20
        # 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
        filters: stat,wall
        # 通过connectProperties属性来打开mergeSql功能;慢SQL记录
        connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000

Docker-SpringBoot-Nginx

SpringBoot项目

bootstrap.yml
server:
  port: 9306
  # 上下文
  servlet:
    context-path: /dxThirdPartyApi

# Spring
spring:
  application:
    # 应用名称
    name: dongxin-third-party-invoking
  profiles:
    # 环境配置
    active: ${spring.profile:dev}
  cloud:
    nacos:
      discovery:
        # 服务注册地址
        server-addr: ${NACOS_SERVER:ip:8848}
#        namespace: 8dc7dc26-0992-407d-8a72-230a02d6212c   # nml
#        namespace: f95f20f9-c801-4801-a06e-0f8197dfc656  # dev
      config:
        # 配置中心地址
        server-addr: ${NACOS_SERVER:ip:8848}
        # 配置文件格式
        file-extension: yml
        # 共享配置
        shared-configs:
          - application-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}
#        namespace: f95f20f9-c801-4801-a06e-0f8197dfc656  # dev

docker配置

docker-compose.yml
version: "3.8"
networks:
  default:
    external:
      name: net_default
services:
  redis:
    image: redis:6.2.5
    restart: always
    ports:
      - "6379"
    environment:
      - TZ=Asia/Shanghai
    volumes:
      - ./redis.conf:/usr/local/etc/redis/redis.conf:rw
      - ./redis-data:/data:rw
    command:
      /bin/bash -c "redis-server /usr/local/etc/redis/redis.conf"
  nacos2:
    image: nacos/nacos-server:2.0.3
    command: ["/wait-for-it.sh", "mysql:3306", "-s", "--", "/home/nacos/bin/docker-startup.sh"]
    env_file:
      - ./env/nacos-standlone-mysql.env
    environment:
      - TZ=Asia/Shanghai
    volumes:
      - ./wait-for-it.sh:/wait-for-it.sh
      - ./nacos-data/logs:/home/nacos/logs
      - ./init.d/custom.properties:/home/nacos/init.d/custom.properties
    ports:
      - "8868:8848"
      - "9868:9848"
      - "9555"
    depends_on:
      - mysql
    restart: always
  mysql:
    image: mysql:8.0.25
    command: --default-authentication-plugin=mysql_native_password
    restart: always
    ports:
      - 3306
    env_file:
      - ./env/mysql.env
    environment:
      - TZ=Asia/Shanghai
    volumes:
      - ./mysql-data/mysql:/var/lib/mysql
      - ./nacos-data/sql:/docker-entrypoint-initdb.d
      
  thirdparty:
      image: openjdk:11.0.11-9-jdk-oracle
      command: ["/wait-for-it.sh", "nacos2:8848", "-s", "--", "java","-jar","dongxin-third-party-invoking.jar"]
      restart: always
      ports:
        - "9306:9306"
      working_dir: /opt/app
      environment:
        - TZ=Asia/Shanghai
        - spring.profiles.active=test
        - NACOS_SERVER=nacos2:8848
      volumes:
        - ./wait-for-it.sh:/wait-for-it.sh
        - ./jars/dongxin-third-party-invoking.jar:/opt/app/dongxin-third-party-invoking.jar
        - ./logs/dongxin-third-party-invoking:/home/data/dongXin-Cloud/logs/dongxin-third-party-invoking      

nginx

topsense.xx.com.conf
log_format topsense.test.dogamedata.com  '$remote_addr - $remote_user [$time_local] $request '
             '$status $body_bytes_sent $http_referer $request_body '
             '$http_user_agent $http_x_forwarded_for';

upstream self-api {
         server ip:9680 max_fails=2 fail_timeout=5s;
}
upstream http_thirdparty {
         server ip:9306 max_fails=2 fail_timeout=20s;
}


server {
   listen 80;
   server_name  topsense.test.dogamedata.com;
   root /home/dogamedata/wwwroot/topsense/dist;

        location ^~ /dxThirdPartyApi/ {
                proxy_pass http://http_thirdparty;
                proxy_redirect off;
                proxy_set_header Host $host;
                proxy_set_header X-Real-IP $remote_addr;
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        }
        # 前端代码中调用后台接口时,默认加上self-api/
        location ^~ /self-api/ {
                proxy_pass http://self-api/;
                proxy_redirect off;
                proxy_set_header Host $host;
                proxy_set_header X-Real-IP $remote_addr;
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        }

        access_log  /home/dogamedata/wwwlogs/topsense.test.dogamedata.com.log topsense.test.dogamedata.com;

}
备注
1 docker
./jars : jars目录的上一级目录;../jars : jars目录的上两级目录;
ports:- "9306:9306" :将docker的内部端口9306和服务的端口9306映射否则在外部访问不到该docker服务;
查看当前服务器端口为9306的服务:[root@58-74 sbin]# netstat -anp|grep 9306

2 nginx
默认目录:/usr/local/nginx/conf/
配置文件目录:/usr/local/nginx/conf/vhosts
sbin目录:/usr/local/nginx/sbin
sbin目录下的命令:
核查配置文件是否合法:./nginx -t
重载配置文件:./nginx -s reload
nginx日志目录:文件里配置的,access_log  /home/dogamedata/wwwlogs/topsense.test.dogamedata.com.log topsense.test.dogamedata.com;

SpringBoot AOP实践

1、创建SpringBoot项目

https://blog.csdn.net/weixin_51309915/article/details/123349773

2、添加aop依赖

        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
        </dependency>

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

3、业务代码块

示例:

public interface UserService {
  void addUser(String username, String password) throws Exception;
}

@Service
public class UserServiceImpl implements UserService{
  @Override
  public void addUser(String username, String password) throws Exception{
    System.out.println(" ...addUser... ");
    throw new Exception("throw new Exception:aop throw");
  }
}

4、aop代码块

package com.example.SpringBoot_demo2.handler;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
import java.util.Arrays;

@Aspect
@Component
public class LogAspect {

  @Before("execution(* com.example.SpringBoot_demo2.*.*.*(..))")
  public void logBefore(JoinPoint joinPoint) {
    Object[] args = joinPoint.getArgs(); // 获取方法参数
    String methodName = joinPoint.getSignature().getName(); // 获取方法名
    System.out.println("Before: method " + methodName + " is called with args " + Arrays.toString(args));
  }

  @AfterReturning("execution(* com.example.SpringBoot_demo2.*.*.*(..))")
  public void logAfter(JoinPoint joinPoint) {
    String methodName = joinPoint.getSignature().getName(); // 获取方法名
    System.out.println("After: method " + methodName + " is finished");
  }

  @AfterThrowing(value = "execution(* com.example.SpringBoot_demo2.*.*.*(..))", throwing = "exception")
  public void logException(JoinPoint joinPoint, Throwable exception) {
    String methodName = joinPoint.getSignature().getName(); // 获取方法名
    System.out.println("Exception: method " + methodName + " throws exception: " + exception.getMessage());
  }

}

@Transactional

@Transactional 是一个注解,用于在 Spring Framework 中标记事务的边界。

通过在方法或类上添加 @Transactional 注解,可以指示 Spring 在方法执行期间管理事务。当标记为 @Transactional 的方法被调用时,Spring 将会创建一个事务,并在方法执行完毕后进行提交或回滚。

例如,在下面的示例中,doSomething() 方法将会在事务的边界内运行:

@Transactional
public void doSomething() {
    // 执行业务操作
}

在这个示例中,当调用 doSomething() 方法时,Spring 将会为该方法创建一个事务。如果方法执行成功,事务将会被提交。如果发生异常,事务将会被回滚,以保证数据的一致性。

@Transactional 注解还支持一些可选的属性,用于配置事务的传播行为、隔离级别、超时等。例如:

@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.DEFAULT, timeout = 10)
public void doSomething() {
    // 执行业务操作
}

在上面的示例中,使用了一些额外的属性来定义事务的传播行为(Propagation.REQUIRED)、隔离级别(Isolation.DEFAULT)和超时时间(timeout = 10)。

值得注意的是,@Transactional 注解通常会在 Spring 管理的组件(如 Service 层或 DAO 层)中使用,以确保数据库操作的数据一致性和事务的正确管理。

希望能够解答你的问题!如果还有其他疑问,请随时提问。

如果事务仍然未生效,可能是由于事务配置不正确或存在其他细节问题。以下是一些可能导致事务未生效的原因和解决方法:

  1. 检查是否正确配置了事务管理器。请确保在Spring配置文件中定义了事务管理器(例如DataSourceTransactionManager)。
  2. 确保handleBoardData方法是通过依赖注入调用的。如果直接在同一个类中的另一个方法中调用handleBoardData,那么事务将不会生效。确保handleBoardData方法是通过注入的方式来调用的。
  3. 检查异常的传播机制。如果在buildDataBoardbuildBoardReport等方法中抛出了异常,但没有被捕获或包装成其他异常,事务将无法回滚。请确保这些方法中的异常被正确捕获并进行处理。
  4. 检查依赖注入是否正确。确保boardReportServiceboardReportRelationServicesqlQueryChartService等依赖项已经正确注入。

代码示例:

package com.idongyou.dongxin.system.handler;
//import packages

@Slf4j
@Component
public class DataBoardHandler {

  @Autowired
  private DataBoardService dataBoardService;
  @Autowired
  private BoardReportService boardReportService;
  @Autowired
  private BoardReportRelationService boardReportRelationService;
  @Autowired
  private SqlQueryChartService sqlQueryChartService;
  @Autowired
  private Snowflake snowflake;

  @Transactional(rollbackFor = Exception.class)
  public void handleBoardData(Long appId, Integer userId, Map<String,Integer> allBoardNameMap, Map<String,Integer> allReportNameMap, List<DataBoard> boardList, List<BoardReport> reportList, List<BoardReportRelation> boardReportRelationList, List<SqlQueryChart> sqlQueryChartList){
    try {
      if (CollectionUtils.isEmpty(boardList)){
        return;
      }
      //重构board数据
      buildDataBoard(appId,userId,allBoardNameMap,boardList);
      //批量存储看板数据
      dataBoardService.saveBatch(boardList);
      if (CollectionUtils.isEmpty(reportList)){
        return;
      }
      //处理报表数据时,生成chartId的旧新映射关系
      Map<String,String> chartIdsMap = new HashMap<>();
      //重构report数据
      buildBoardReport(appId,userId,allReportNameMap,reportList,chartIdsMap);
      //批量存储报表数据
      boardReportService.saveBatch(reportList);

      //生成旧新id映射map
      Map<Long,Long> boardIdsMap = boardList.stream().collect(Collectors.toMap(DataBoard::getOldBoardId, DataBoard::getBoardId));
      Map<Long,Long> reportIdsMap = reportList.stream().collect(Collectors.toMap(BoardReport::getOldReportId,BoardReport::getReportId));
      //重构看板报表关系数据
      buildBoardReportRelation(boardIdsMap,reportIdsMap,boardReportRelationList);
      //批量存储看板报表关系数据
      boardReportRelationService.saveBatch(boardReportRelationList);

      if (CollectionUtils.isEmpty(sqlQueryChartList)){
        return;
      }
      //重构SqlQueryChart列表数据
      buildSqlQueryChart(appId,userId,sqlQueryChartList,reportIdsMap,chartIdsMap);
      //批量存储SqlQueryChart数据
      sqlQueryChartService.saveBatch(sqlQueryChartList);
    } catch (Exception e) {
      log.error("Exception error", e);
      throw new RuntimeException("操作失败");
    }
  }

  //重构SqlQueryChart对象数据
  public void buildSqlQueryChart(Long appId,Integer userId,List<SqlQueryChart> sqlQueryChartList,Map<Long,Long> reportIdsMap,Map<String,String> chartIdsMap){
    //do something
  }

  //重构BoardReportRelation数据
  public void buildBoardReportRelation(Map<Long,Long> boardIdsMap,Map<Long,Long> reportIdsMap,List<BoardReportRelation> boardReportRelationList){
    //do something
  }

  //重构BoardReport对象数据
  public void buildBoardReport(Long appId,Integer userId,Map<String,Integer> allReportNameMap,List<BoardReport> reportList,Map<String,String> chartIdsMap){
 	//do something
  }

  //重构DataBoard对象数据
  public void buildDataBoard(Long appId,Integer userId,Map<String,Integer> allBoardNameMap,List<DataBoard> boardList){
   //do something
  }

}

@Slf4j
@Service
public class DataBoardServiceImpl extends ServiceImpl<DataBoardMapper, DataBoard> implements DataBoardService {
    @Autowired
    private DataBoardHandler dataBoardHandler;
  
    @Override
    public void importBoardsData(MultipartFile file,Long appId) throws Exception{
        SysImportExportFileInfo sysImportExportFileInfo = new SysImportExportFileInfo();
        try {
        //do something
dataBoardHandler.handleBoardData(appId,userId,allBoardNameMap,allReportNameMap,boardList,reportList,boardReportRelationList,sqlQueryChartList);
            //保存文件上传成功记录
            sysImportExportFileInfo.setStatus(1);//成功
            sysImportExportFileInfoService.saveOne(sysImportExportFileInfo);
        }catch (IOException ioe){
            //保存文件上传失败记录
            sysImportExportFileInfo.setStatus(0);//失败
            sysImportExportFileInfoService.saveOne(sysImportExportFileInfo);
            throw ioe;
        }catch (Exception e) {
            //保存文件上传失败记录
            sysImportExportFileInfo.setStatus(0);//失败
            sysImportExportFileInfoService.saveOne(sysImportExportFileInfo);
            throw e;
        }
    }
}
@EnableTransactionManagement
@EnableScheduling
@SpringBootApplication(exclude = MongoAutoConfiguration.class)
@EnableDiscoveryClient
@EnableDongXinSwagger2
@EnableDongXinFeignClients
public class DongXinSystemApplication {
    public static void main(String[] args) {
        SpringApplication.run(DongXinSystemApplication.class, args);
    }
}

3 组件整合应用

Java消费Kafka

在Java中,Kafka消费数据有以下几种方式:

  1. 手动提交偏移量并轮询

该方式需要在代码中明确管理和提交偏移量,并且需要轮询以获取新的消息。一般情况下,这种方式比较适用于需要更精细控制的场景。

while (true) {
    ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));
    for (ConsumerRecord<String, String> record : records) {
        // 处理收到的消息
    }
    consumer.commitSync();
}
  1. 订阅模式(自动提交偏移量)

该方式是指定一个消费者组,让Kafka自动管理和提交偏移量,以及周期性地提交已接收到的消息的偏移量。使用这种方式可以更轻松地进行扩展和管理,但是需要注意的是,在某些失败情况下,可能会存在少量消息重复处理或者消息丢失的风险。

consumer.subscribe(Arrays.asList("topic1", "topic2"));
while (true) {
    ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));
    for (ConsumerRecord<String, String> record : records) {
        // 处理收到的消息
    }
}
  1. 分配模式(手动提交偏移量)

该方式需要指定消费者分配所需的分区,并管理和提交偏移量。可以使用该方法来更精细控制分区处理。但是需要注意的是,在某些失败情况下,可能会存在少量消息重复处理或者消息丢失的风险。

TopicPartition partition0 = new TopicPartition("topic1", 0);
TopicPartition partition1 = new TopicPartition("topic1", 1);
consumer.assign(Arrays.asList(partition0, partition1));
while (true) {
    ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));
    for (ConsumerRecord<String, String> record : records) {
        // 处理收到的消息
    }
    consumer.commitSync();
}

以上就是在Java中Kafka消费数据的几种方式,您可以根据自己的需要进行选择。注意,在使用任意一种方式时,都要记得关闭消费者以释放资源。

应用实例:

1 实现MessageListener

定义抽象类实现MessageListener

示例:定义抽象类BaseConsumer实现MessageListener

import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.springframework.kafka.listener.MessageListener;

public abstract class BaConsumer implements MessageListener<String,String> {

  /**
   * kafka topic数据监听
   * */
  @Override
  public void onMessage(ConsumerRecord<String, String> stringStringConsumerRecord) {

    try {
      doConsumer("消费逻辑");
    } catch (Exception e) {
      e.printStackTrace();
    }

  }

  /**
   * kafka数据的消费逻辑
   * */
  public abstract void doConsumer(String s) throws Exception;
}
2 实现数据消费逻辑

继承抽象消费类实现数据消费逻辑

示例:

public class SynDataForMysqlWorker extends BaseConsumer {
    public SynDataForMysqlWorker(WorkerParam workerParam) {
        super(workerParam);
    }
    
    @Override
    public void doConsumer(MessageInfo messageInfo) throws Exception {
    //数据消费逻辑
    }
    
}
补充

kafka配置参数

示例:

@Configuration
@Getter
public class ApplicationYml {
    @Value("${warn-bootstrap-servers:192.168.0.177:9091,192.168.0.178:9091,192.168.0.179:9091}")
    private String warnBootstrapServers;
    @Value("${exception-collector.topic:topsense-exception}")
    private String exceptionTopic;
    @Value("${spring.datasource.druid.url}")
    private String dbUrl;
    //消费数据的来源
    @Value("${customize.consumer-type:DEBEZIUM}")
    private String consumerType;
    @Value("${customize.target-database-type:CLICKHOUSE}")
    private String targetDatabaseType;

    //kafka的topic、表名等相关配置
    @Value("${customize.kafka.topic_table_file}")
    private String topicTableFile;
    //源数据库相关配置
    @Value("${customize.kafka.source_db_file}")
    private String sourceDbFile;
    @Value("${customize.kafka.partition}")
    private int partition;
    @Value("${customize.kafka.groupId}")
    private String groupId;
    @Value("${customize.kafka.offset_file_dir}")
    private String kafkaOffsetFileDir;
 }   

整合mongoDb多数据源

在Spring Boot中实现MongoDB的多数据源配置,可以使用MongoClientMongoTemplate来实现。以下是一种常见的多数据源配置方式:

  1. 导入相关依赖: 在pom.xml文件中添加Spring Data MongoDB的依赖:
xmlCopy Code<dependencies>
    <!-- 其他依赖... -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-mongodb</artifactId>
    </dependency>
</dependencies>
  1. 创建MongoDB配置类: 创建一个配置类来配置多个MongoDB数据源,例如:
import com.mongodb.ConnectionString;
import com.mongodb.client.MongoClients;
import com.mongodb.client.MongoClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.mongodb.core.MongoTemplate;

@Configuration
public class MongoConfig {
    @Value("${mongodb1.uri}")
    private String mongoUri1;

    @Value("${mongodb2.uri}")
    private String mongoUri2;

    @Bean(name = "mongoTemplate1")
    public MongoTemplate mongoTemplate1() {
        ConnectionString connectionString = new ConnectionString(mongoUri1);
        MongoClient mongoClient = MongoClients.create(connectionString);
        return new MongoTemplate(mongoClient, connectionString.getDatabase());
    }

    @Bean(name = "mongoTemplate2")
    public MongoTemplate mongoTemplate2() {
        ConnectionString connectionString = new ConnectionString(mongoUri2);
        MongoClient mongoClient = MongoClients.create(connectionString);
        return new MongoTemplate(mongoClient, connectionString.getDatabase());
    }
}

在上述示例中,我们通过@Value注解读取配置文件中的多个MongoDB连接URI,并创建不同的MongoTemplate对象。

  1. 配置数据源连接信息: 在application.properties(或application.yml)文件中配置MongoDB连接信息,例如:
propertiesCopy Codemongodb1.uri=mongodb://localhost:27017/db1
mongodb2.uri=mongodb://localhost:27017/db2

yamlCopy Codemongodb1:
  uri: mongodb://localhost:27017/db1

mongodb2:
  uri: mongodb://localhost:27017/db2
  1. 使用多个数据源: 在需要使用的地方注入对应的MongoTemplate,并使用它进行数据库操作。示例:
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.stereotype.Repository;

@Repository
public class UserRepository {
    private final MongoTemplate mongoTemplate1;
    private final MongoTemplate mongoTemplate2;

    public UserRepository(@Qualifier("mongoTemplate1") MongoTemplate mongoTemplate1,
                          @Qualifier("mongoTemplate2") MongoTemplate mongoTemplate2) {
        this.mongoTemplate1 = mongoTemplate1;
        this.mongoTemplate2 = mongoTemplate2;
    }

    public User saveUser1(User user) {
        return mongoTemplate1.save(user);
    }

    public User saveUser2(User user) {
        return mongoTemplate2.save(user);
    }

    // 其他操作方法...
}

在上述示例中,我们通过构造函数注入不同的MongoTemplate对象,并使用它们来执行对应的数据库操作。

通过以上步骤,您就可以在Spring Boot项目中实现MongoDB的多数据源配置和使用了。

整合mysql

package com.idongyou.dongxin.system.datasource;

import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import javax.sql.DataSource;

@Configuration
//配置mybatis的接口类放的地方
@MapperScan(basePackages = "com.idongyou.dongxin.system.mapper", sqlSessionFactoryRef = "masterSqlSessionFactory")
public class MysqlDataSourceConfig {

	@Autowired
	private MybatisPlusInterceptor mybatisPlusInterceptor;

	@Primary
	// 将这个对象放入Spring容器中
	@Bean(name = "masterDataSource")
	// 表示这个数据源是默认数据源
	// 读取application.properties中的配置参数映射成为一个对象
	// prefix表示参数的前缀
	@ConfigurationProperties(prefix = "spring.datasource.dynamic.datasource.master")
	public DataSource getDateSource1() {
		return DruidDataSourceBuilder.create().build();
	}

	@Bean(name = "masterSqlSessionFactory")
	// 表示这个数据源是默认数据源
	// @Qualifier表示查找Spring容器中名字为masterDataSource的对象
	public MybatisSqlSessionFactoryBean iProcessSqlSessionFactory(
			@Qualifier("masterDataSource") DataSource datasource) throws Exception {
		MybatisSqlSessionFactoryBean sqlSessionFactoryBean = new MybatisSqlSessionFactoryBean();
		sqlSessionFactoryBean.setDataSource(getDateSource1());
		MybatisConfiguration configuration = new MybatisConfiguration();
		//configuration.setMapUnderscoreToCamelCase(false);
		sqlSessionFactoryBean.setConfiguration(configuration);
		sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:/mapper/system/**/*.xml"));

		//设置 MyBatis-Plus 分页插件
		Interceptor[] plugins = {mybatisPlusInterceptor};
		sqlSessionFactoryBean.setPlugins(plugins);

		return sqlSessionFactoryBean;

	}

	@Bean(name = "masterTransactionManager")
	public DataSourceTransactionManager getTransactionManager(@Qualifier("masterDataSource") DataSource dataSource) {
		return new DataSourceTransactionManager(dataSource);
	}

	@Bean("masterSqlSessionTemplate")
	// 表示这个数据源是默认数据源
	public SqlSessionTemplate test1sqlsessiontemplate(
			@Qualifier("masterSqlSessionFactory") SqlSessionFactory sessionfactory) {
		return new SqlSessionTemplate(sessionfactory);
	}
}

整合doris

package com.idongyou.dongxin.system.datasource;

import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.core.JdbcTemplate;
import javax.sql.DataSource;

@Configuration
@MapperScan(basePackages = "com.idongyou.dongxin.system.dao.doris",sqlSessionFactoryRef = "dorisSqlSessionFactory")
public class DorisDataSourceConfig {

	@Autowired
	private MybatisPlusInterceptor mybatisPlusInterceptor;

	@Bean(name = "dorisTemplate")
	public JdbcTemplate dorisJdbcTemplate(@Qualifier("dorisDataSource") DataSource dataSource) {
		return new JdbcTemplate(dataSource);
	}

	@Bean(name = "dorisDataSource")
	@ConfigurationProperties(prefix ="spring.datasource.druid.doris")
	public DataSource dorisDataSource() {
		return DruidDataSourceBuilder.create().build();
	}

	@Bean(name = "dorisSqlSessionFactory")
	// 表示这个数据源是默认数据源
	// @Qualifier表示查找对象
	public MybatisSqlSessionFactoryBean cstSqlSessionFactory(@Qualifier("dorisDataSource") DataSource datasource) throws Exception {
		MybatisSqlSessionFactoryBean sqlSessionFactoryBean = new MybatisSqlSessionFactoryBean();
		sqlSessionFactoryBean.setDataSource(dorisDataSource());
		//这里如果用mybatis plus的话,要用mybatis-plus的configuration
		MybatisConfiguration configuration = new MybatisConfiguration();
		//configuration.setMapUnderscoreToCamelCase(false); 
		sqlSessionFactoryBean.setConfiguration(configuration);
		sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:/mapper/doris/*.xml"));

		//设置 MyBatis-Plus 分页插件
		Interceptor[] plugins = {mybatisPlusInterceptor};
		sqlSessionFactoryBean.setPlugins(plugins);
		return sqlSessionFactoryBean;
	}

	@Bean("dorisSqlSessionTemplate")
	public SqlSessionTemplate test1sqlsessiontemplate(
			@Qualifier("dorisSqlSessionFactory") SqlSessionFactory sessionfactory) {
		return new SqlSessionTemplate(sessionfactory);
	}
	
}

整合clickhouse

package com.idongyou.dongxin.system.datasource;

import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.core.JdbcTemplate;
import javax.sql.DataSource;


@Configuration
@MapperScan(basePackages = "com.idongyou.dongxin.system.dao.clickhouse",sqlSessionFactoryRef = "clickhouseSqlSessionFactory")
public class ClickhouseDataSourceConfig {

	@Autowired
	private MybatisPlusInterceptor mybatisPlusInterceptor;

	@Bean(name = "clickhouseTemplate")
	public JdbcTemplate clickhouseJdbcTemplate(@Qualifier("clickhouseDataSource") DataSource dataSource) {
		return new JdbcTemplate(dataSource);
	}

	@Bean(name = "clickhouseDataSource")
	@ConfigurationProperties(prefix ="spring.datasource.druid.clickhouse")
	public DataSource clickhouseDataSource() {
		return DruidDataSourceBuilder.create().build();
	}

	@Bean(name = "clickhouseSqlSessionFactory")
	// 表示这个数据源是默认数据源
	// @Qualifier表示查找对象
	public MybatisSqlSessionFactoryBean cstSqlSessionFactory(@Qualifier("clickhouseDataSource") DataSource datasource) throws Exception {
		MybatisSqlSessionFactoryBean sqlSessionFactoryBean = new MybatisSqlSessionFactoryBean();
		sqlSessionFactoryBean.setDataSource(clickhouseDataSource());
		//这里如果用mybatis plus的话,要用mybatis-plus的configuration
		MybatisConfiguration configuration = new MybatisConfiguration();
		//configuration.setMapUnderscoreToCamelCase(false); 
		sqlSessionFactoryBean.setConfiguration(configuration);
		sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:/mapper/clickhouse/*.xml"));

		//设置 MyBatis-Plus 分页插件
		Interceptor[] plugins = {mybatisPlusInterceptor};
		sqlSessionFactoryBean.setPlugins(plugins);
		return sqlSessionFactoryBean;
	}

	@Bean("clickhouseSqlSessionTemplate")
	public SqlSessionTemplate sqlsessiontemplate(
			@Qualifier("clickhouseSqlSessionFactory") SqlSessionFactory sessionfactory) {
		return new SqlSessionTemplate(sessionfactory);
	}
	
}

整合Redis

在Spring Boot项目中整合Redis,可以使用Spring Data Redis来简化操作。下面是一些基本步骤:

  1. 添加依赖:在项目的pom.xml文件中添加Spring Data Redis的依赖。
xmlCopy Code<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
  1. 配置Redis连接信息:在application.properties或application.yml中配置Redis连接信息。
yamlCopy Codespring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.password=xxx
  1. 创建Redis配置类:创建一个Redis配置类,用于配置RedisTemplate和连接工厂。
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericToStringSerializer;

@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);
        template.setValueSerializer(new GenericToStringSerializer<>(Object.class));
        return template;
    }
}

上述配置类中,使用RedisTemplate作为操作Redis的核心对象,并设置连接工厂以及值的序列化器。

  1. 使用RedisTemplate进行操作:在需要使用Redis的地方使用@Autowired注入RedisTemplate,并通过它来进行Redis的操作。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;

@Service
public class MyService {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    public void setKey(String key, Object value) {
        redisTemplate.opsForValue().set(key, value);
    }

    public Object getValue(String key) {
        return redisTemplate.opsForValue().get(key);
    }

    public boolean deleteKey(String key) {
        return redisTemplate.delete(key);
    }
}

上述示例中,使用RedisTemplate的opsForValue()方法获取操作字符串值的对象,然后使用set()、get()和delete()等方法进行相应的操作。

这样,你就可以在Spring Boot项目中使用Redis进行缓存、数据存储等相关操作了。

需要注意的是,以上仅为简单示例,实际应用中可能需要更复杂的操作和配置,可以根据具体需求进行调整。另外,确保Redis服务已正常启动,并根据实际情况修改Redis连接配置信息。

应用实例:

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisClusterConfiguration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericToStringSerializer;
import java.time.Duration;
import java.util.Arrays;

@Configuration
public class RedisConfig2 {

  @Value("${spring.redis.cluster.nodes}")
  private String clusterNodes;

  @Bean(name = "redisTemplate2")
  public RedisTemplate<String, Object> redisTemplate(@Qualifier("redisConnectionFactory2") RedisConnectionFactory connectionFactory) {
    RedisTemplate<String, Object> template = new RedisTemplate<>();
    template.setConnectionFactory(connectionFactory);
    template.setValueSerializer(new GenericToStringSerializer<>(Object.class));
    return template;
  }

  /**
   * lettuce
   * */
  @Bean(name = "redisConnectionFactory2")
  public LettuceConnectionFactory redisConnectionFactory() {
    RedisClusterConfiguration config = new RedisClusterConfiguration(Arrays.asList(clusterNodes.split(",")));
    LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder()
        .commandTimeout(Duration.ofSeconds(2))
        .build();
    return new LettuceConnectionFactory(config, clientConfig);
  }

}

Java+Flink应用

项目:盘古数据分层

1 JOB开发流程

1 初始化环境;

2 加载数据源;

3 处理数据;

4 输出数据;

5 执行;

            // 1 初始化环境
            NewEnvironmentUtils utils = NewEnvironmentUtils.getInstance(args);
            // 创建流执行环境
            StreamExecutionEnvironment env = utils.getENV();
            // 加载表环境
            StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);

            // 2 加载数据源,从kafka消费汇总日期
            DataStream<AdsAggreData> adsData = env.addSource(new FlinkKafkaConsumer<>(EnvParameterConfig.adsAdReportTopics,
                    new AdsAggreDataKafkaDeserializationSchema(), EnvParameterConfig.consumeProperties)).setParallelism(1);

            // 3 处理数据
            adsData = adsData
                    .keyBy((KeySelector<AdsAggreData, String>) AdsAggreData::getReport_day)
                    .process(new AdReportDelayProcessFunction());
            adsData.print("adsData");
            // 将汇总日期、汇总日期的历史数据key创建成临时视图
            DataStream<AdsAggreData> allReportDay = createTempViewFromDataStream(tableEnv, adsData, argData);
            // 设置Table自定义函数
            createUdf(tableEnv);
            // 广告数据分析报表(日)数据汇总
            DataStream<AdAnalysisReportDoris> adDayDataStream = calculateDayData(adsData, allReportDay, tableEnv, argData);
            
			// 4 输出数据:设置数据Sink
			adDayDataStream.addSink(getJdbcSink()).name("adDayDataStream");

            // 定时任务:定期发送数据到kafka
            KafkaSink<String> createRoleSink = KafkaConfigUtil.buildSink(EnvParameterConfig.productProperties, EnvParameterConfig.adsAdReportTopics, new SimpleStringSchema());
            DataStreamSource<String> taskDataSource = env.addSource(new AdReportSourceFunction(argData));
            taskDataSource.setParallelism(1).sinkTo(createRoleSink).name("AdReportSourceFunction");

			//5 执行Job
            env.execute("AdAnalysisReportTableJob");

2 核心API

2.1 StreamExecutionEnvironment

StreamExecutionEnvironment 是 Apache Flink 中用于设置执行环境和配置作业的类。它提供了一系列的核心方法,用于创建和操作流处理环境。以下是一些常用的核心方法:

  1. getExecutionEnvironment(): 获取当前可用的执行环境。在 Flink 应用程序中,可以通过调用该方法获取到一个 StreamExecutionEnvironment 的实例。
  2. fromCollection(Collection<T> collection): 从给定的 Java 集合中创建数据流。可以将内存中的集合作为输入源,方便进行测试和调试。
  3. fromElements(T... elements): 直接指定一组元素并将它们转换为数据流。可以方便地创建固定的数据流。
  4. readTextFile(String filePath): 从文本文件中逐行读取数据,并返回一个字符串类型的数据流。
  5. addSource(SourceFunction<T> sourceFunction): 使用用户提供的 SourceFunction 从数据源读取数据,并返回一个指定类型的数据流。
  6. setParallelism(int parallelism): 设置整个作业的并行度。并行度决定了作业中算子的并行执行程度,可以通过该方法为整个作业设置统一的并行度。
  7. execute(String jobName): 提交作业并执行。作业名称通过参数传入。
  8. executeAsync(String jobName): 异步提交作业,并返回一个 JobExecutionResult,可以用于获取作业执行的状态、结果等信息。
  9. getCheckpointConfig(): 获取作业的检查点配置,可以通过该方法对作业的检查点进行配置和管理。
  10. enableCheckpointing(long interval): 开启作业的检查点功能,并设置检查点的触发间隔时间。

这些核心方法提供了一些常用的功能,可以根据需要对流处理环境进行配置和操作,包括创建数据流、设置并行度、提交作业等。

以下是一个示例,展示如何使用 StreamExecutionEnvironment 进行流处理作业的配置和提交:

StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

// 设置并行度
env.setParallelism(2);

// 从文本文件读取数据
DataStream<String> stream = env.readTextFile("file:///path/to/file.txt");

// 进行数据转换和计算操作
DataStream<Integer> result = stream
    .flatMap((String line, Collector<Integer> out) -> {
        // 执行数据转换逻辑
        // 将每行字符串转换为整数并输出到 Collector 中
        out.collect(Integer.parseInt(line));
    })
    .map(num -> num * 2);

// 提交作业并执行
env.execute("MyJob");

在上述示例中,首先通过 getExecutionEnvironment() 方法获取一个 StreamExecutionEnvironment 实例。然后,通过 setParallelism() 方法设置作业的并行度为 2。接下来,使用 readTextFile() 方法从文本文件中读取数据,并将其转换为字符串类型的数据流。之后,通过一系列的转换操作对数据流进行处理,并最终得到一个整数类型的结果数据流。最后,通过 execute() 方法提交作业并执行。

以上是 StreamExecutionEnvironment 类的一些核心方法,可以根据具体的需求使用适当的方法配置和管理流处理作业。

1、fromSource

fromSource(SourceFunction<T> sourceFunction, WatermarkStrategy<T> watermarkStrategy): fromSource() 方法是一个更灵活的方法,提供了额外的参数来定义数据源和水位线策略。除了实现 SourceFunction 接口外,还可以通过 fromSource() 方法传递一个 WatermarkStrategy 参数来定义水位线的生成方式。WatermarkStrategy 可以通过时间戳、周期性生成或任何其他自定义逻辑来生成水位线。

2、addSource

addSource(SourceFunction<T> sourceFunction): addSource() 方法是一个简便的方法,用于将实现了 SourceFunction 接口的自定义数据源添加到流处理环境,并返回相应类型的数据流。SourceFunction 接口需要用户手动实现 run()cancel() 方法来定义从数据源产生数据和停止条件。addSource() 方法会自动将 SourceFunction 转化为一个算子,并将其添加到作业图中。

2.2 StreamTableEnvironment

StreamTableEnvironment 是 Apache Flink 中用于操作和处理流数据的表格环境。它提供了一系列方法,用于创建、注册、查询和转换表格数据。以下是一些常用的方法:

  1. create(StreamExecutionEnvironment env): 创建一个 StreamTableEnvironment 实例,与给定的流处理环境关联。
  2. fromDataStream(DataStream<T> dataStream): 将 DataStream 转换为对应的 Table 对象,以便在后续的查询中使用。
  3. toAppendStream(Table table, Class<T> clazz): 将 Table 对象转换为 DataStream<T>,只输出新增记录。
  4. toRetractStream(Table table, Class<Tuple2<Boolean, T>> clazz): 将 Table 对象转换为 DataStream<Tuple2<Boolean, T>>,其中第一个字段表示记录的插入或删除操作,第二个字段表示具体记录的值。
  5. registerDataStream(String name, DataStream<T> dataStream): 注册一个 DataStream 到表格环境中,以便在 SQL 查询中使用。
  6. registerTable(String name, Table table): 注册一个已存在的表格到表格环境中,以便在 SQL 查询中使用。
  7. executeSql(String query): 执行一条 SQL 查询,并返回一个对应的结果表格。
  8. sqlQuery(String query): 执行一条 SQL 查询,并返回一个对应的结果表格,与 executeSql() 方法功能相同,只是返回类型不同。
  9. toAppendStream(String query, Class<T> clazz): 将一条 SQL 查询的结果转换为 DataStream<T>,只输出新增记录。
  10. toRetractStream(String query, Class<Tuple2<Boolean, T>> clazz): 将一条 SQL 查询的结果转换为 DataStream<Tuple2<Boolean, T>>,其中第一个字段表示记录的插入或删除操作,第二个字段表示具体记录的值。
  11. createTemporaryView(String name, Table table): 创建一个临时视图,将一个表格注册到表格环境中供查询使用。
  12. dropTemporaryView(String name): 删除一个临时视图。
  13. listTables(): 列出当前表格环境中的所有表格名称。
  14. getConfig(): 获取当前表格环境的配置信息。
  15. registerFunction(String name, ScalarFunction function): 注册自定义的标量函数到表格环境中,以便在 SQL 查询中使用。

这些方法提供了一些常用的功能,可以根据需要创建、注册、查询和转换表格数据。通过这些方法,你可以使用 SQL 查询、转换流数据并与其他 Flink 的数据处理操作集成。

注意:上述方法是基于 Apache Flink 1.13 版本的 StreamTableEnvironment,未来版本可能会有所变化,请查阅官方文档以获取最新的方法和用法信息。

这些核心方法提供了一些常用的功能,可以根据需求创建、注册、查询和转换表格数据,并进行流处理和分析。

以下是一个示例,展示如何使用 StreamTableEnvironment 进行表格数据的注册、查询和转换:

StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);

// 创建一个 DataStream 并注册到表格环境
DataStream<Row> dataStream = env.fromElements(Row.of("Alice", 25), Row.of("Bob", 30));
tableEnv.registerDataStream("myDataStream", dataStream, "name, age");

// 执行一条 SQL 查询并获取结果表格
Table resultTable = tableEnv.sqlQuery("SELECT name, age FROM myDataStream WHERE age > 25");

// 将结果转换为 DataStream 并输出
DataStream<Tuple2<Boolean, Row>> resultStream = tableEnv.toRetractStream(resultTable, Row.class);
resultStream.print();

// 提交作业并执行
env.execute("MyJob");

在上述示例中,首先通过 StreamExecutionEnvironment 创建一个流处理环境,然后使用该环境创建一个 StreamTableEnvironment 实例。接着,使用 registerDataStream() 方法将一个 DataStream 注册到表格环境中,指定数据流的名称和字段信息。然后,通过 sqlQuery() 方法执行一条 SQL 查询,并返回一个结果表格。最后,通过 toRetractStream() 方法将结果转换为 DataStream<Tuple2<Boolean, Row>> 并输出。最终,通过调用 execute() 方法提交作业并执行。

以上是 StreamTableEnvironment 类的一些常用核心方法,可以根据具体需求使用适当的方法来操作和处理表格数据。

1、create

create(StreamExecutionEnvironment env): 根据给定的流处理环境创建一个 StreamTableEnvironment 实例。可以通过该方法将流处理环境与表格环境进行关联。

2、registerTable

registerTable(String name, Table table): 注册一个已经存在的表格到表格环境中,以便后续可以在 SQL 查询中使用。需要为表格指定一个名称。

3、executeSql

executeSql(String query): 执行一条 SQL 查询,并返回一个对应的结果表格。

4、createTemporaryView

创建一个临时视图,将一个表格注册到表格环境中供查询使用。

createTemporaryViewStreamTableEnvironment 的一个方法,它用于在 Flink SQL 中创建一个临时视图。临时视图是基于流数据的逻辑表,可以在后续的 SQL 查询中使用。

以下是 createTemporaryView 方法的基本语法:

public void createTemporaryView(String viewName, Table table)

参数说明:

  • viewName:临时视图的名称,用于在后续的 SQL 查询中引用。
  • table:要创建视图的表。

示例代码如下所示:

StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
StreamTableEnvironment tEnv = StreamTableEnvironment.create(env);

// 创建一个 DataStream,并转换成 Table
DataStream<Tuple2<String, Integer>> stream = ... // 输入数据流
Table table = tEnv.fromDataStream(stream, $("name"), $("age"));

// 创建临时视图
tEnv.createTemporaryView("my_view", table);

// 在后续的 SQL 查询中使用临时视图
Table result = tEnv.sqlQuery("SELECT name, age FROM my_view WHERE age > 18");

在上述示例中,首先通过 fromDataStream 方法将输入的 DataStream 转换为 Table 对象。然后,使用 createTemporaryView 方法将该表注册为临时视图,指定视图名称为 “my_view”。接下来,可以在后续的 SQL 查询中使用 my_view 这个临时视图进行数据查询和处理。

请注意,createTemporaryView 创建的临时视图只在当前会话中有效,并且在关闭会话时会自动删除。如果需要长期保存视图,可以使用 createCatalogView 方法将视图注册到持久化的外部目录(如 Hive 或其他支持的存储系统)中。

应用:

    private static DataStream<AdsAggreData> createTempViewFromDataStream(StreamTableEnvironment tableEnv, DataStream<AdsAggreData> adsData, ParameterTool argData) {
        Schema keySchema = getKeySchema();
        // 广告数据分析报表的key(日)
        DataStream<AdAnalysisReportDoris> dayHistoryKeys = adsData.flatMap(new AdReportKeyFunction(argData, true));
        tableEnv.createTemporaryView("ad_report_day_key_flink", dayHistoryKeys, keySchema);
        // 广告数据分析报表的key(小时)
        DataStream<AdAnalysisReportDoris> hourHistoryKeys = adsData.flatMap(new AdReportKeyFunction(argData, false));
        tableEnv.createTemporaryView("ad_report_hour_key_flink", hourHistoryKeys, keySchema);
        // 根据活跃信息找出对应的激活时间
        DataStream<AdsAggreData> activeRegDataStream = adsData.flatMap(new AdReportRegDaysByActFunction(argData, true));
        tableEnv.createTemporaryView("report_day_flink", activeRegDataStream);
        print(activeRegDataStream, argData, "activeRegDataStream");
        return activeRegDataStream;
    }
5、registerFunction

registerFunction(String name, ScalarFunction function): 注册自定义的标量函数到表格环境中,以便在 SQL 查询中使用。

6、createTemporarySystemFunction

在 Apache Flink 中,使用 StreamTableEnvironmentcreateTemporarySystemFunction 方法可以创建临时的系统函数。系统函数是预定义的函数,可以在 SQL 或 Table API 中使用。

以下是使用 createTemporarySystemFunction 方法创建临时系统函数的示例代码:

import org.apache.flink.table.api.DataTypes;
import org.apache.flink.table.api.EnvironmentSettings;
import org.apache.flink.table.api.TableEnvironment;
import org.apache.flink.table.functions.ScalarFunction;

public class TemporarySystemFunctionExample {
    public static void main(String[] args) {
        // 创建 StreamTableEnvironment
        EnvironmentSettings settings = EnvironmentSettings.newInstance().useBlinkPlanner().inStreamingMode().build();
        TableEnvironment tableEnv = TableEnvironment.create(settings);

        // 注册一个临时的系统函数
        tableEnv.createTemporarySystemFunction("myUpper", MyUpper.class);

        // 使用临时系统函数
        tableEnv.executeSql("SELECT myUpper(name) FROM MyTable");
    }

    // 自定义系统函数
    public static class MyUpper extends ScalarFunction {
        public String eval(String s) {
            if (s == null) {
                return null;
            }
            return s.toUpperCase();
        }
    }
}

上述示例中,我们首先创建了一个 StreamTableEnvironment,然后使用 createTemporarySystemFunction 方法注册了一个名为 “myUpper” 的临时系统函数,并指定了自定义的函数类 MyUpper。接下来,我们可以在 SQL 查询中使用 myUpper 函数,如示例中的 SELECT 语句所示。

请注意,在示例中,我们定义了一个名为 MyUpper 的自定义函数类,继承自 ScalarFunction。在 eval 方法中,我们实现了函数的逻辑,将输入的字符串转换为大写。

通过 createTemporarySystemFunction 方法注册的临时系统函数只在当前会话有效,不会被持久化保存。如果需要在多个会话中共享系统函数,可以考虑使用 createTemporaryFunction 方法或 createFunction 方法进行函数的注册。

2.3 DataStream

DataStream 是 Apache Flink 中用于处理流式数据的主要概念之一,它提供了一系列操作和转换方法,用于对数据流进行处理和分析。以下是 DataStream 的一些核心方法:

  1. 转换操作:
    • map(Function): 对数据流中的每个元素应用给定的函数,并生成一个新的数据流。
    • filter(FilterFunction): 根据给定的条件筛选数据流中的元素,并生成一个新的数据流。
    • flatMap(FlatMapFunction): 将每个输入元素转换为零个或多个输出元素,并生成一个新的数据流。
    • keyBy(KeySelector): 将数据流按照指定的键进行分组,生成一个键控数据流。
    • reduce(ReduceFunction): 对键控数据流中的元素进行归约操作,生成一个新的数据流。
    • window(WindowAssigner): 将数据流划分为不同的窗口,并生成窗口控制的数据流。
  2. 时间相关操作:
    • assignTimestampsAndWatermarks(AssignerWithPeriodicWatermarks): 为数据流中的元素分配时间戳和水位线,用于事件时间处理。
    • timeWindow(Time): 基于处理时间的滚动窗口操作,将数据流划分为指定时间长度的窗口。
    • eventTimeWindow(Time, Time): 基于事件时间的滚动窗口操作,将数据流划分为指定事件时间范围的窗口。
  3. 窗口函数:
    • apply(WindowFunction): 对窗口中的元素应用给定的窗口函数,并生成一个新的数据流。
  4. Sink 操作:
    • addSink(SinkFunction): 将数据流发送到给定的 SinkFunction 中,例如将数据写入文件、发送到 Kafka 主题等。

除了上述核心方法外,DataStream 还提供了许多其他方法,如连接操作、广播操作、异步操作等,用于更复杂的数据流处理场景。它还支持状态管理、事件时间处理、容错机制、迭代操作等高级功能,以满足不同的流处理需求。

请注意,具体可用的方法取决于所使用的 Flink 版本和 API 版本。以上列出的是一些常见的核心方法,可能会因版本而有所不同。

map
filter
flatMap
keyBy
reduce
window
1、SlidingEventTimeWindows

SlidingEventTimeWindows 是 Apache Flink 流处理框架中提供的一个窗口类型,用于基于事件时间的滑动窗口计算。与滚动窗口相比,滑动窗口可以重叠并且具有更灵活的窗口定义。

SlidingEventTimeWindows 的构造函数需要两个参数:

  • size:窗口的大小,即窗口持续的时间长度。
  • slide:窗口的滑动步长,即窗口之间的时间间隔。

下面是一个示例代码片段,演示如何使用 SlidingEventTimeWindows 来计算一个滑动窗口内元素的总和:

DataStream<Tuple2<String, Integer>> dataStream = ...; // 输入数据流

dataStream.keyBy(0)
    .window(SlidingEventTimeWindows.of(Time.seconds(10), Time.seconds(5)))
    .sum(1)
    .print();

在上述示例中,首先按键进行分组(keyBy()),然后使用滑动事件时间窗口(window())将数据流划分为大小为 10 秒、滑动步长为 5 秒的窗口。接下来,使用 sum(1) 对窗口内元素的第二个字段进行求和操作。最后,通过 print() 将结果打印出来。

需要注意的是,在使用 SlidingEventTimeWindows 时,需要导入相应的依赖包,并确保你的 Flink 版本支持该窗口类型。此外,还需要根据实际场景设置合适的窗口大小和滑动步长,以确保满足你的业务需求。

assignTimestampsAndWatermarks
timeWindow
apply
addSink
2.4 DataStreamSource

DataStreamSource 是 Apache Flink 中用于创建数据流的接口之一,用于从不同的数据源读取数据并将其转换为数据流。它是表示数据流的起始点,可以从文件、集合、消息队列等各种数据源中读取数据。

DataStreamSource 接口定义了多个方法来读取不同类型的数据源:

  1. 从文件读取数据:
    • readTextFile(String path): 从文本文件中逐行读取数据,并返回一个字符串类型的数据流。
    • readFile(FileInputFormat<T> inputFormat, String filePath): 使用用户提供的 FileInputFormat 从文件中读取数据,并返回一个指定类型的数据流。
  2. 从集合读取数据:
    • fromCollection(Collection<T> collection): 从 Java 集合中读取数据,并返回一个指定类型的数据流。
    • fromParallelCollection(SplittableIterator<T> collection): 从可拆分迭代器中并行读取数据,并返回一个指定类型的数据流。
  3. 从消息队列(如 Kafka)读取数据:
    • addSource(SourceFunction<T> function): 使用用户提供的 SourceFunction 从消息队列读取数据,并返回一个指定类型的数据流。
  4. 从自定义数据源读取数据:
    • addSource(SourceFunction<T> function): 通过实现用户自定义的 SourceFunction 接口来读取数据,并返回一个指定类型的数据流。

以上仅列举了常用的 DataStreamSource 方法,实际上还有其他方法可以根据不同的需求读取数据源。使用这些方法,可以方便地将不同类型的数据源转换为数据流,从而进行后续的流处理和分析。

以下是一个示例,演示如何从文本文件创建一个字符串类型的数据流:

DataStream<String> stream = env.readTextFile("file:///path/to/file.txt");

其中,envStreamExecutionEnvironment 对象,用于设置执行环境和配置 Flink 作业。readTextFile() 方法从指定的文本文件中读取数据,并返回一个字符串类型的数据流。可以根据需要对返回的数据流进行进一步的操作和处理。

以下是一个示例,展示如何使用 addSource() 方法从自定义数据源读取数据:

public class MySource implements SourceFunction<Integer> {
    private volatile boolean isRunning = true;
    
    @Override
    public void run(SourceContext<Integer> ctx) throws Exception {
        while (isRunning) {
            // 从数据源产生数据
            int data = generateData();
            ctx.collect(data);
            Thread.sleep(1000); // 模拟每秒产生一条数据
        }
    }
    
    @Override
    public void cancel() {
        isRunning = false;
    }
}

// 创建数据流并从自定义数据源读取数据
DataStream<Integer> stream = env.addSource(new MySource());

在上述示例中,首先定义了一个实现了 SourceFunction 接口的自定义数据源 MySource,其中的 run() 方法通过 collect() 方法产生数据,并在循环中模拟每秒产生一条数据。然后,使用 addSource() 方法将 MySource 作为参数传入,创建一个整数类型的数据流 stream

以上是 DataStreamSource 接口的一些核心方法,可以根据具体的需求选择合适的方法来构建数据流。

2.5 AsyncDataStream

AsyncDataStream 是 Apache Flink 提供的用于处理异步流数据的工具类,其中核心的方法是 unorderedWait()orderedWait()。这两个方法将同步流数据转换为异步流,并提供了异步处理的功能。

下面分别介绍这两个核心方法的参数和功能:

  1. unorderedWait(DataStream<IN> input, AsyncFunction<IN, OUT> asyncFunction, long timeout, TimeUnit timeUnit, int capacity): 这个方法将输入的同步流 input 转换为异步流,并使用提供的 AsyncFunction 对象进行异步处理。主要参数包括:

    • input: 输入的同步流 DataStream<IN>
    • asyncFunction: 实现了 AsyncFunction 接口的异步函数对象,用于执行异步处理。
    • timeout: 异步操作的超时时间,以指定的时间单位计量。
    • timeUnit: 超时时间的单位,可以是 TimeUnit.MILLISECONDSTimeUnit.SECONDS 等。
    • capacity: 异步缓冲区的容量,用于控制异步操作的并发度。

    这个方法返回一个异步流 DataStream<OUT>,其中的记录顺序可能与输入流不完全一致。

  2. orderedWait(DataStream<IN> input, AsyncFunction<IN, OUT> asyncFunction, long timeout, TimeUnit timeUnit, int capacity): 这个方法与 unorderedWait() 类似,但它会尝试维护输入顺序,即使异步操作完成的顺序与输入流不一致,结果流中的记录也会按输入流的顺序进行排序。

    参数和返回值与 unorderedWait() 相同。

这两个方法可以通过传入实现了 AsyncFunction 接口的异步函数对象来执行异步处理。AsyncFunction 接口定义了 asyncInvoke 方法,用于执行异步操作。在自定义的异步函数中,可以根据需求编写异步逻辑,例如与外部系统的异步调用、数据库查询等。

下面是一个示例,展示如何使用 unorderedWait() 方法进行异步流处理:

StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

DataStream<String> input = env.fromElements("A", "B", "C", "D");

AsyncFunction<String, String> asyncFunction = new MyAsyncFunction();

DataStream<String> output = AsyncDataStream.unorderedWait(input, asyncFunction, 5000, TimeUnit.MILLISECONDS, 100);

output.print();

env.execute("AsyncJob");

在上述示例中,我们创建了一个输入流 input,其中包含了一些字符串记录。然后,定义了一个实现了 AsyncFunction 接口的自定义异步函数 MyAsyncFunction,用于模拟异步处理。接下来,通过调用 unorderedWait() 方法,将输入流转换为异步流,并传入异步函数、超时时间和缓冲区容量等参数。最后,通过 print() 方法打印异步处理的结果。最终,我们使用 execute() 方法提交作业并执行。

可以根据具体需求来选择 unorderedWait()orderedWait() 方法。如果结果流的顺序不重要,可以使用 unorderedWait() 获得更好的性能。如果需要维护输入顺序,可以使用 orderedWait() 方法。

希望这个示例能够帮助你理解 AsyncDataStream 核心方法的使用和功能。

1、unorderedWait
2、orderedWait

3 核心接口和类

1、ParameterTool

Java 的 ParameterTool 类是 Apache Flink 提供的一个实用工具类,用于处理和解析应用程序运行时的命令行参数。它提供了一种方便的方式来读取、访问和管理应用程序所需的参数。

ParameterTool 类可以用于从命令行、配置文件、系统属性等多个来源解析和获取参数。它提供了以下常用方法:

  1. fromArgs:从命令行参数中解析获取参数值,参数以键值对的形式传递。例如:--input input.txt --output output.txt
  2. fromPropertiesFile:从一个或多个配置文件中加载参数,并将它们解析为键值对。
  3. fromSystemProperties:从 Java 系统属性中获取参数,通过 -Dkey=value 形式设置系统属性。
  4. get:通过参数名获取参数值。
  5. has:检查是否存在某个参数。

使用 ParameterTool 类的示例代码如下:

import org.apache.flink.api.java.utils.ParameterTool;

public class MyApp {

  public static void main(String[] args) {

    // 解析命令行参数
    ParameterTool params = ParameterTool.fromArgs(args);

    // 获取参数值
    String input = params.get("input");
    String output = params.get("output");

    // 使用参数进行应用程序逻辑处理
    // ...

  }
}

通过以上代码,我们可以轻松地解析命令行参数,并使用 ParameterTool 对象获取参数的值,在应用程序中进行相应的处理。

总结而言,Java 的 ParameterTool 类提供了一个便捷的方式来解析和获取应用程序运行时的命令行参数。使用 ParameterTool 类可以使代码更加模块化和可配置,并且可以方便地适应不同的运行环境。

2、ProducerConfig

ProducerConfig 是 Apache Kafka 中用于配置生产者(Producer)的类。它提供了一组配置选项,用于设置生产者的行为、连接参数、序列化方式等。

要使用 ProducerConfig 类,你需要导入 org.apache.kafka.clients.producer.ProducerConfig 包。

以下是一些常见的 ProducerConfig 配置选项:

  1. bootstrap.servers:指定 Kafka 集群的地址和端口。格式为 host1:port1,host2:port2,...
  2. key.serializer:指定键的序列化器,将键对象转换为字节数组。例如,org.apache.kafka.common.serialization.StringSerializer
  3. value.serializer:指定值的序列化器,将值对象转换为字节数组。例如,org.apache.kafka.common.serialization.StringSerializer
  4. acks:指定消息发送的确认机制。可选值包括 "all"(所有副本都确认消息写入)、"0"(不需要确认)和 "1"(只需集群中的一个副本确认即可)。
  5. retries:指定发送失败时的消息重试次数。
  6. batch.size:指定批量发送消息的大小,以字节为单位。
  7. linger.ms:指定当有未发送消息时,生产者等待的时间阈值,以毫秒为单位。
  8. max.in.flight.requests.per.connection:指定在单个连接上允许的最大未完成请求数。
  9. compression.type:指定消息的压缩类型,例如 "none""gzip""snappy"

可以通过以下方式创建一个 ProducerConfig 对象,并使用它配置 Kafka 生产者:

import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.common.serialization.StringSerializer;

Properties props = new Properties();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());

// 添加其他配置选项...

KafkaProducer<String, String> producer = new KafkaProducer<>(props);

在以上示例中,我们使用 Properties 对象设置了一些常见的配置选项,并将其传递给 KafkaProducer 构造函数,从而创建了一个 Kafka 生产者。

通过 ProducerConfig 类,你可以根据需要配置和调整 Kafka 生产者的行为。请注意,这只是一些常用的配置选项,Kafka 还提供了更多的配置选项,可以根据具体需求进行设置。

希望能对你理解 Java 中的 ProducerConfig 有所帮助。如果有任何进一步的问题,请随时提问!

3、Connection

在 Java 中使用 Apache Flink 建立数据库连接,您可以使用 Flink 提供的 JDBC Connector。JDBC Connector 可以让您在 Flink 中使用 JDBC 连接到各种关系型数据库,并执行 SQL 查询和更新操作。

4、Configuration

在 Apache Flink 中,可以使用 org.apache.flink.configuration.Configuration 类来进行配置管理。Configuration 类提供了一种将键值对存储为配置项的方式,您可以在作业中使用这些配置项。

5、PreparedStatement

在 Java 中使用 Apache Flink 的 PreparedStatement,您需要使用 Flink 提供的 JDBC Connector。JDBC Connector 可以让您在 Flink 中使用 JDBC 连接到各种关系型数据库,并执行 SQL 查询和更新操作。

下面是一个简单的示例,演示如何在 Flink 中使用 PreparedStatement 执行 SQL 查询:

import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.api.common.ExecutionConfig;
import org.apache.flink.api.common.functions.FlatMapFunction;
import org.apache.flink.api.common.functions.RichFlatMapFunction;
import org.apache.flink.api.common.state.ValueState;
import org.apache.flink.api.common.state.ValueStateDescriptor;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.util.Collector;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;

public class PreparedStatementExample {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

        // 创建连接并准备查询
        Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/database", "username", "password");
        String query = "SELECT name, age FROM users WHERE age > ?";

        // 从 JDBC 数据源创建数据流
        DataStream<Integer> thresholdStream = env.fromElements(18);

        // 执行查询操作
        DataStream<Tuple2<String, Integer>> resultStream = thresholdStream.flatMap(new QueryExecution(conn, query));

        // 打印结果
        resultStream.print();

        env.execute("PreparedStatement Example");
    }
}

// FlatMapFunction 用于执行 SQL 查询
class QueryExecution extends RichFlatMapFunction<Integer, Tuple2<String, Integer>> {
    private transient PreparedStatement statement;
    private transient Connection connection;

    private final String query;

    public QueryExecution(Connection connection, String query) {
        this.connection = connection;
        this.query = query;
    }

    @Override
    public void open(Configuration parameters) throws Exception {
        super.open(parameters);
        statement = connection.prepareStatement(query);
    }

    @Override
    public void close() throws Exception {
        super.close();
        if (statement != null) {
            statement.close();
        }
    }

    @Override
    public void flatMap(Integer value, Collector<Tuple2<String, Integer>> out) throws Exception {
        statement.setInt(1, value);

        try (ResultSet resultSet = statement.executeQuery()) {
            while (resultSet.next()) {
                String name = resultSet.getString("name");
                int age = resultSet.getInt("age");
                out.collect(new Tuple2<>(name, age));
            }
        }
    }
}

在上述示例中,我们首先通过 DriverManager.getConnection 方法建立与数据库的连接,并准备待执行的 SQL 查询。然后,我们使用 env.fromElements 来创建一个包含查询参数的数据流 thresholdStream

接下来,我们实现了一个自定义的 QueryExecution 类作为 RichFlatMapFunction,用于在 flatMap 方法中执行 SQL 查询。在 open 方法中,我们初始化了 PreparedStatement,并在 flatMap 方法中将查询参数设置为输入的值。然后,我们通过 executeQuery 方法执行 SQL 查询,将结果解析为 Tuple2 对象,并通过 out.collect 发送到下游。

最后,我们在 main 方法中将数据流 resultStream 打印出来,并使用 execute 方法来触发整个 Flink 程序的执行。

请注意,您需要将上述代码中的数据库连接 URL、用户名和密码替换为实际的数据库信息。同时,确保您已在 Flink 的环境中包含相应的 JDBC 驱动程序依赖项。

这只是一个简单的示例,您可以根据自己的需求进行扩展并执行更复杂的 SQL 操作。

6、KafkaRecordDeserializationSchema

KafkaRecordDeserializationSchema 是 Apache Flink 提供的用于将 Kafka 记录(record)反序列化为对象的接口。它可以帮助您在 Flink 中将从 Kafka 主题消费的数据转换为具体的 Java 对象,以便进行后续的处理和分析。

KafkaRecordDeserializationSchema 接口定义了以下方法:

  1. T deserialize(ConsumerRecord<byte[], byte[]> record) throws Exception:该方法用于将 Kafka 记录反序列化为指定类型的对象。您需要根据实际情况实现此方法,根据 ConsumerRecord 中的字节数组数据,将其转换成您期望的对象类型 T
  2. boolean isEndOfStream(T nextElement):该方法用于判断是否到达 Kafka 主题的末尾。在 Flink 中,如果数据源(如 Kafka)到达末尾,Flink 会自动触发作业的停止。因此,您可以通过实现这个方法来检查反序列化后的对象是否表示了 Kafka 主题的末尾。

下面是一个简单的示例,演示如何实现一个自定义的 KafkaRecordDeserializationSchema

public class MyDeserializationSchema implements KafkaRecordDeserializationSchema<MyObject> {

    @Override
    public MyObject deserialize(ConsumerRecord<byte[], byte[]> record) throws Exception {
        // 将字节数组反序列化为 MyObject 对象的逻辑
        // 返回反序列化后的对象
    }

    @Override
    public boolean isEndOfStream(MyObject nextElement) {
        // 返回是否到达 Kafka 主题的末尾的逻辑
        // 如果返回 true,Flink 会触发作业的停止
    }

    @Override
    public TypeInformation<MyObject> getProducedType() {
        // 返回 MyObject 类型的 TypeInformation,用于 Flink 运行时系统推断类型信息
    }
}

在上述示例中,您需要实现 deserialize() 方法来定义如何将 ConsumerRecord 中的字节数组反序列化为自定义的 MyObject。通过实现 isEndOfStream() 方法,您可以根据需要确定是否已经到达了 Kafka 主题的末尾。getProducedType() 方法用于提供 MyObject 的类型信息,以便 Flink 运行时系统能够正确地推断数据类型。

使用自定义的 KafkaRecordDeserializationSchema,您可以通过 Flink 提供的 KafkaConsumer 将其应用到数据源中,例如:

StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
Properties props = new Properties();
// 配置 Kafka 的连接属性

// 创建 KafkaConsumer,并设置反序列化模式
KafkaConsumer<MyObject> kafkaConsumer = new KafkaConsumer<>(props, new MyDeserializationSchema());

// 从 Kafka 主题消费数据
DataStream<MyObject> stream = env.addSource(kafkaConsumer);
// 对数据流进行后续处理

通过以上示例,您可以使用自定义的 KafkaRecordDeserializationSchema 将 Kafka 主题的记录反序列化为指定类型的对象,并将其引入到 Flink 的数据流作业中进行处理。

7、KafkaDeserializationSchema

KafkaDeserializationSchema 是 Apache Flink 提供的用于将 Kafka 消息反序列化为对象的接口。它可以帮助您在 Flink 中将从 Kafka 主题消费的数据转换为具体的 Java 对象,以便进行后续的处理和分析。

KafkaDeserializationSchema 接口定义了以下方法:

  1. T deserialize(ConsumerRecord<byte[], byte[]> record) throws Exception:该方法用于将 Kafka 消息反序列化为指定类型的对象。您需要根据实际情况实现此方法,根据 ConsumerRecord 中的字节数组数据,将其转换成您期望的对象类型 T
  2. boolean isEndOfStream(T nextElement):该方法用于判断是否到达 Kafka 主题的末尾。在 Flink 中,如果数据源(如 Kafka)到达末尾,Flink 会自动触发作业的停止。因此,您可以通过实现这个方法来检查反序列化后的对象是否表示了 Kafka 主题的末尾。
  3. TypeInformation<T> getProducedType():该方法用于返回反序列化后的对象类型的 TypeInformation。Flink 运行时系统使用这个类型信息来推断数据类型和执行相关的优化。

下面是一个简单的示例,演示如何实现一个自定义的 KafkaDeserializationSchema

public class MyDeserializationSchema implements KafkaDeserializationSchema<MyObject> {

    @Override
    public MyObject deserialize(ConsumerRecord<byte[], byte[]> record) throws Exception {
        // 将字节数组反序列化为 MyObject 对象的逻辑
        // 返回反序列化后的对象
    }

    @Override
    public boolean isEndOfStream(MyObject nextElement) {
        // 返回是否到达 Kafka 主题的末尾的逻辑
        // 如果返回 true,Flink 会触发作业的停止
    }

    @Override
    public TypeInformation<MyObject> getProducedType() {
        // 返回 MyObject 类型的 TypeInformation,用于 Flink 运行时系统推断类型信息
    }
}

在上述示例中,您需要实现 deserialize() 方法来定义如何将 ConsumerRecord 中的字节数组反序列化为自定义的 MyObject。通过实现 isEndOfStream() 方法,您可以根据需要确定是否已经到达了 Kafka 主题的末尾。getProducedType() 方法用于提供 MyObject 的类型信息,以便 Flink 运行时系统能够正确地推断数据类型。

使用自定义的 KafkaDeserializationSchema,您可以通过 Flink 提供的 FlinkKafkaConsumer 将其应用到数据源中,例如:

StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
Properties props = new Properties();
// 配置 Kafka 的连接属性

// 创建 FlinkKafkaConsumer,并设置反序列化模式
FlinkKafkaConsumer<MyObject> kafkaConsumer = new FlinkKafkaConsumer<>("my-topic", new MyDeserializationSchema(), props);

// 从 Kafka 主题消费数据
DataStream<MyObject> stream = env.addSource(kafkaConsumer);
// 对数据流进行后续处理

通过以上示例,您可以使用自定义的 KafkaDeserializationSchema 将 Kafka 主题的消息反序列化为指定类型的对象,并将其引入到 Flink 的数据流作业中进行处理。

应用:

package com.test.flink.deserializer;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.test.flink.model.AdsAggreData;
import com.test.flink.utils.DateUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.flink.api.common.typeinfo.TypeHint;
import org.apache.flink.api.common.typeinfo.TypeInformation;
import org.apache.flink.streaming.connectors.kafka.KafkaDeserializationSchema;
import org.apache.kafka.clients.consumer.ConsumerRecord;

import java.nio.charset.StandardCharsets;
import java.sql.Date;

@Slf4j
public class AdsAggreDataKafkaDeserializationSchema implements KafkaDeserializationSchema<AdsAggreData> {
    @Override
    public boolean isEndOfStream(AdsAggreData nextElement) {
        return false;
    }

    @Override
    public AdsAggreData deserialize(ConsumerRecord<byte[], byte[]> consumerRecord) throws Exception {
        if (consumerRecord.value() == null) {
            log.error("AdsAggreDataKafkaDeserializationSchema 接收到消息队列中的数据为空。 topic={}, partition={}, offset={}", consumerRecord.topic(), consumerRecord.partition(), consumerRecord.offset());
            return null;
        }

        String record = new String (consumerRecord.value(), StandardCharsets.UTF_8);
        log.info("消费到的kafka数据为:{}", record);
        if(StringUtils.isNotBlank(record)){
//            AdsAggreData json = JSONObject.parseObject(record, AdsAggreData.class);
//            if (StringUtils.isNotBlank(json.getReport_day())) {
//                return json;
//            }
            AdsAggreData data = new AdsAggreData();
            JSONObject json = JSON.parseObject(record);
            String report_day = json.getString("report_day");
            Long report_mills = json.getLong("report_mills");
            if (StringUtils.isBlank(report_day)) {
                return null;
            }
            if (report_mills == null) {
                report_mills = System.currentTimeMillis();
            }
            try {
                java.util.Date date = DateUtils.stringToDate(report_day, DateUtils.YYYY_MM_DD);
                Date sqlDate = new Date(date.getTime());
                data.setReport_day(report_day);
                data.setReport_day_num(Integer.parseInt(DateUtils.dateToString(date, DateUtils.YYYYMMDD)));
                data.setReport_date(sqlDate);
                data.setReport_mills(report_mills);
                log.info("AdsAggreData数据为:{}", JSON.toJSONString(data));
                return data;
            } catch (Exception e) {
                log.error("广告数据分析宽表-修复日期转换发生异常", e);
                return null;
            }
        }
        return null;
    }

    @Override
    public TypeInformation<AdsAggreData> getProducedType() {
        return TypeInformation.of(new TypeHint<AdsAggreData>(){});
    }
}
8、KeySelector

KeySelector 是 Apache Flink 中的一个接口,用于将数据流按照指定的键进行分组或分区。它定义了如下方法:

public interface KeySelector<IN, KEY> extends Serializable {
    KEY getKey(IN value) throws Exception;
}

KeySelector 接口中,IN 表示输入元素的类型,KEY 表示键的类型。getKey() 方法用于从输入元素中提取用于分组或分区的键值。

使用 KeySelector 接口可以实现对数据流进行基于键的操作,如按照某个字段的值进行分组、分区等。例如,如果有一个包含 Person 对象的数据流,要按照人的年龄对数据流进行分组,可以定义一个实现了 KeySelector<Person, Integer> 接口的类,重写 getKey(Person value) 方法,提取出年龄作为键:

public class AgeKeySelector implements KeySelector<Person, Integer> {
    @Override
    public Integer getKey(Person value) {
        return value.getAge();
    }
}

然后可以在 Flink 的操作中使用该 KeySelector 对数据流进行分组操作,例如:

DataStream<Person> input = ...;
DataStream<Tuple2<Integer, Iterable<Person>>> groupedStream = input
    .keyBy(new AgeKeySelector())
    .groupByKey();

这样,数据流将根据 AgeKeySelector 提取的年龄键进行分组,生成以年龄作为键的分组数据流。在后续的操作中,可以对分组后的数据流进行聚合、计算等操作。

KeySelector 在 Flink 中的应用非常广泛,它是实现基于键的操作的重要方式,如分组、分区、连接、窗口操作等都可以使用 KeySelector 来指定键。

9、ResultFuture

RichAsyncFunctionasyncInvoke() 方法中,你可以通过 ResultFuture 参数来发送异步操作的结果。

ResultFuture 是一个用于将异步计算结果发送到输出收集器的对象。你可以使用它的 complete() 方法来发送计算结果,并使用 completeExceptionally() 方法来发送异常情况。

下面是一个示例,展示了如何在 asyncInvoke() 方法中使用 ResultFuture 发送异步操作的结果:

import org.apache.flink.streaming.api.functions.async.RichAsyncFunction;
import org.apache.flink.util.Collector;
import org.apache.flink.util.concurrent.FutureUtils;

import java.util.concurrent.CompletableFuture;

public class MyAsyncFunction extends RichAsyncFunction<String, Integer> {

    @Override
    public void asyncInvoke(String input, Collector<Integer> collector) throws Exception {
        // 根据实际需求,对输入元素进行异步处理

        CompletableFuture<Integer> future = // 异步操作,例如与外部系统交互或数据库查询
                CompletableFuture.supplyAsync(() -> Integer.parseInt(input) * 2);

        // 处理异步计算结果
        future.whenComplete((result, error) -> {
            if (error != null) {
                // 如果出现异常情况,则发送异常
                collector.getFuture().completeExceptionally(error);
            } else {
                // 发送异步计算结果
                collector.collect(result);
            }
        });
    }
}

在上述示例中,我们使用 CompletableFuture 来模拟一个异步操作,其中将输入字符串转换为整数并进行一些计算。然后,在 whenComplete() 方法中,我们处理异步计算的结果和异常情况。

如果异步操作成功完成,我们使用 collector.collect(result) 将计算结果发送到输出收集器。

如果在异步操作过程中出现了异常,我们使用 collector.getFuture().completeExceptionally(error) 发送异常情况。

collector.getFuture() 方法返回一个 CompletableFuture 对象,该对象表示异步操作的结果。可以使用它的方法获取异步操作的结果或处理异常情况。

这是使用 ResultFuture 参数发送异步操作结果的一种常用方式,可以根据实际需求进行调整。

10、ScheduledExecutorService

ScheduledExecutorService 是 Java 中用于执行计划任务的接口。它是 ExecutorService 的子接口,提供了一些额外的方法,可以在指定延迟后或按周期性调度任务的执行。

ScheduledExecutorService 可以用来替代传统的 Timer 类,提供更强大和灵活的计划任务执行功能。

下面是一个简单示例,展示如何使用 ScheduledExecutorService 来调度任务的执行:

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class ScheduledExecutorServiceExample {
    public static void main(String[] args) {
        // 创建 ScheduledExecutorService
        ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);

        // 延迟执行任务
        executorService.schedule(() -> {
            System.out.println("任务执行");
        }, 5, TimeUnit.SECONDS);

        // 定期执行任务
        executorService.scheduleAtFixedRate(() -> {
            System.out.println("定期任务执行");
        }, 0, 1, TimeUnit.SECONDS);

        // 关闭 ScheduledExecutorService
        executorService.shutdown();
    }
}

在上述示例中,我们使用 Executors.newScheduledThreadPool(1) 创建了一个包含一个线程的 ScheduledExecutorService

然后,我们使用 schedule() 方法在 5 秒后执行一个任务。这意味着任务将在当前时间的基础上延迟 5 秒后执行。

接下来,我们使用 scheduleAtFixedRate() 方法设置一个定期执行的任务。参数中的 0 表示任务立即开始执行,1 表示每隔 1 秒重复执行一次。

最后,我们调用 shutdown() 方法来关闭 ScheduledExecutorService,以确保程序的正常退出。

请注意,ScheduledExecutorService 在执行计划任务时依赖于线程池,因此需要在不再使用时显式地关闭它,以释放关联的资源。

11、Schema

在 Apache Flink 中,Schema 是用来描述数据流的格式和结构的定义。Flink 提供了多种方式来定义和处理数据流的 Schema。

以下是一些常见的 Java Flink Schema 相关操作:

  1. 使用基本类型或 POJO 类型定义 Schema:
DataStream<Integer> stream = ...; // 输入数据流
DataStream<Tuple2<String, Integer>> result = stream
    .map(new MyMapFunction()); // 自定义 MapFunction,将输入数据转换为 Tuple2 类型
  1. 使用 Avro Schema 定义 Schema:
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setRestartStrategy(RestartStrategies.noRestart());

// 配置 Avro 序列化/反序列化所需的注册信息
ConfluentRegistryAvroDeserializationSchema<MyAvroRecord> deserializationSchema =
    new ConfluentRegistryAvroDeserializationSchema<>(MyAvroRecord.class, schemaRegistryURL);
deserializationSchema.setFailOnMissingField(true);

Properties properties = new Properties();
properties.setProperty("bootstrap.servers", "localhost:9092");
properties.setProperty("group.id", "flink-group");

// 创建 Kafka 数据源
FlinkKafkaConsumer<MyAvroRecord> kafkaConsumer = new FlinkKafkaConsumer<>(
    "my_topic",
    deserializationSchema,
    properties);

// 添加 Kafka 数据源
DataStream<MyAvroRecord> stream = env.addSource(kafkaConsumer);
  1. 使用 JSON Schema 定义 Schema:
DataStream<String> stream = ...; // 输入数据流
DataStream<Tuple2<String, Integer>> result = stream
    .map(new JsonToTupleMapFunction()); // 自定义 MapFunction,将输入数据解析为 Tuple2 类型
  1. 使用 Table Schema 定义 Schema:
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);
TableSchema schema = TableSchema.builder()
    .field("name", DataTypes.STRING())
    .field("age", DataTypes.INT())
    .build();

// 将 DataStream 转换为 Table
DataStream<Tuple2<String, Integer>> stream = ...;
tableEnv.createTemporaryView("my_table", stream, schema);

// 执行 Table API 或 SQL 查询
Table result = tableEnv.from("my_table");

通过这些方式,您可以根据数据流的类型和来源来定义适当的 Schema,以便在 Flink 中进行处理和转换。

12、MapState

MapState 是 Apache Flink 流处理框架中的一种状态类型,用于在算子函数中保存键值对类型的状态。它可以用于存储和访问与特定键相关联的状态数据。

MapState 的特点包括:

  • 键值对形式的状态:MapState 以键值对的方式组织状态数据。
  • 动态更新:可以随时更新键值对,包括添加、删除和修改操作。
  • 键空间隔离:MapState 支持按照键进行区分,不同键之间的状态是相互独立的。

以下是一个使用 MapState 的示例代码片段,假设我们有一个数据流,其中包含用户的姓名和年龄信息:

DataStream<Tuple2<String, Integer>> dataStream = ...; // 输入数据流

dataStream.keyBy(0)
    .process(new CountAgeProcessFunction())
    .print();

public class CountAgeProcessFunction extends KeyedProcessFunction<String, Tuple2<String, Integer>, String> {
    private MapState<String, Integer> countState;

    @Override
    public void open(Configuration parameters) throws Exception {
        countState = getRuntimeContext().getMapState(new MapStateDescriptor<>("countState", String.class, Integer.class));
    }

    @Override
    public void processElement(Tuple2<String, Integer> value, Context ctx, Collector<String> out) throws Exception {
        String name = value.f0;
        Integer age = value.f1;

        // 根据姓名在状态中获取年龄计数
        Integer count = countState.get(name);
        if (count == null) {
            count = 0;
        }

        // 更新状态中的年龄计数
        countState.put(name, count + 1);

        out.collect("Name: " + name + ", Age: " + age + ", Count: " + (count + 1));
    }
}

在上述示例中,首先按姓名进行分组(keyBy()),然后使用 CountAgeProcessFunction 对每个姓名进行处理。CountAgeProcessFunction 中定义了一个 MapState 类型的变量 countState,通过 getRuntimeContext().getMapState() 方法来获取具体的 MapState 实例。

processElement() 方法中,我们根据输入元素的姓名从 MapState 中获取对应的年龄计数。如果不存在该姓名的计数,则默认为 0。然后通过 put() 方法更新 MapState 中的年龄计数。最后通过 out.collect() 发送结果。

使用 MapState 需要导入相应的依赖包,并确保你的 Flink 版本支持该状态类型。

4 核心Function

在 Apache Flink 中,核心的函数接口主要分为以下几类:

  1. Transformation Functions(转换函数):这些函数用于对输入流进行转换和处理,常见的函数接口包括 MapFunctionFilterFunctionFlatMapFunction 等。它们分别用于对单个元素进行映射、过滤和扁平化操作。
  2. Key Selection Functions(键选择函数):当使用键控操作(例如 keyBy)对数据流进行分组时,需要指定如何选择键。常见的键选择函数接口包括 KeySelectorKeyedSerializationSchema 等。
  3. Aggregation Functions(聚合函数):用于对分组后的数据流进行聚合操作,例如 ReduceFunctionAggregateFunction 等。这些函数可以计算子组或窗口的聚合结果。
  4. Window Functions(窗口函数):用于将数据分配到固定大小的窗口,并进行窗口内的聚合计算,例如 WindowFunctionProcessWindowFunction 等。这些函数在窗口关闭时被触发,可以访问窗口中所有元素。
  5. Process Functions(处理函数):提供了对每个元素进行处理的灵活机制,例如 ProcessFunctionKeyedProcessFunction 等。这些函数可以维护状态、注册定时器,并提供了更复杂的事件处理和状态管理能力。
  6. Sink Functions(输出函数):用于将数据流输出到外部系统,例如 SinkFunctionRichSinkFunction 等。这些函数定义了将数据发送到外部系统的行为。
  7. Source Functions(输入函数):用于从外部系统读取数据并生成数据流,例如 SourceFunctionRichSourceFunction 等。这些函数定义了从外部系统读取数据的行为。

此外,Flink 还提供了丰富的窗口类型和触发器函数、连接函数、合并函数等,以满足不同的计算需求。

这些核心的函数接口可以根据具体的需求和业务逻辑进行组合和使用,使得 Flink 能够进行灵活的数据处理和计算。

4.1 Source Functions
1、SourceFunction
2、RichSourceFunction
4.2 Sink Functions

在 Flink 中,有多种常用的 Sink 函数可用于将数据流写入不同的目标。以下是一些常见的 Sink 函数:

  1. PrintSinkFunction:该函数用于将数据流的元素打印到标准输出或日志中,对调试和快速观察数据流很有帮助。
  2. CollectSinkFunction:这个函数将数据流的元素收集到一个列表中,并暴露一个方法来获取收集到的元素列表。
  3. SocketTextStreamSinkFunction:该函数将数据流的字符串形式写入到指定的套接字(Socket)中,可以用于将数据发送到外部系统或进行简单的网络通信。
  4. CassandraSink:Flink 提供了与 Apache Cassandra 数据库集成的 Sink 函数,可以将数据写入到 Cassandra 表中。
  5. JDBCOutputFormat/JDBCSinkFunction:这些函数用于将数据流写入关系型数据库,如 MySQL、PostgreSQL 等。通过 JDBC 连接,可以将数据插入到数据库表中。
  6. ElasticsearchSinkFunction:这个函数用于将数据流写入到 Elasticsearch 集群中,方便进行实时搜索和分析。
  7. FileSinkFunction:该函数将数据流写入到文件系统中的文件,可以按行或按指定格式进行文件写入。
  8. KafkaSink:Flink 提供了与 Apache Kafka 集成的 Sink 函数,可以将数据流写入到 Kafka 主题中。

这只是列举了一些常见的 Sink 函数,还有其他的 Sink 函数可供选择。可以根据具体需求选择和实现自定义的 Sink 函数。

请注意,在使用这些 Sink 函数时,需要配置相应的连接信息、序列化器、并发度等参数,以便与目标系统适配和优化数据写入操作。

1、JdbcSink

JdbcSink 是 Apache Flink 提供的一个用于将数据流写入关系型数据库的 Sink 函数。它是 org.apache.flink.streaming.api.functions.sink.SinkFunction 接口的一个实现类。

使用 JdbcSink 可以方便地将数据流的元素写入到关系型数据库中的表中。它提供了以下几个重要的方法:

  1. Builder 构造器:通过 JdbcSink.sink() 方法创建一个 Builder 对象,并指定数据库连接信息和插入语句。
  2. setParameterTypes(Class<?>... parameterTypes):设置 SQL 语句中占位符的参数类型,以便在写入时进行校验和序列化。
  3. setBatchSize(int batchSize):设置批量写入的大小。当达到指定数量的元素时,会将这些元素一次性批量写入数据库,以提高写入的效率。
  4. setSqlTypes(int... sqlTypes):设置数据库表中对应插入语句占位符的数据类型。
  5. setSqlStatementProvider(SqlStatementProvider<T> provider):设置提供插入语句的自定义 SqlStatementProvider
  6. setRowOperationMapper(RowOperationMapper<T>):设置自定义的 RowOperationMapper,用于将数据流的元素映射为数据库表的一行。
  7. build():构建 JdbcSink 对象。

使用 JdbcSink 可以通过调用 addSink(JdbcSink) 方法将其应用到数据流上,并指定需要写入的数据库表。例如:

DataStream<MyData> dataStream = ...; // 输入数据流

JdbcSink.sink(
    "INSERT INTO my_table (col1, col2) VALUES (?, ?)", // 插入语句
    (ps, value) -> {
        ps.setString(1, value.getField1());
        ps.setInt(2, value.getField2());
    }) // RowOperationMapper
    .setDataSource(new JdbcConnectionPool(...)) // 设置数据库连接池
    .build(); // 构建 JdbcSink 对象

dataStream.addSink(jdbcSink); // 将 Sink 应用到数据流上

以上示例中,插入语句 "INSERT INTO my_table (col1, col2) VALUES (?, ?)" 会将 MyData 数据流的元素插入到名为 my_table 的数据库表中的对应列中。

需要注意的是,使用 JdbcSink 需要引入相关的依赖,例如 JDBC 驱动等。

sink方法的用法:

JdbcSink.sink() 方法是用于创建 JdbcSink.Builder 对象的静态方法。它是构建 JdbcSink 的入口点。

下面是使用 JdbcSink.sink() 方法的示例代码:

JdbcSink.sink(
    sqlStatement,
    jdbcStatementBuilder,
    jdbcConnectionOptions
);

该方法接受三个参数:

  1. sqlStatement:插入语句或更新语句,用于指定数据写入数据库时要执行的 SQL 语句。例如:“INSERT INTO my_table (col1, col2) VALUES (?, ?)”。
  2. jdbcStatementBuilder:一个 JdbcStatementBuilder 或 lambda 函数,用于设置 PreparedStatement 中的占位符参数。你可以使用预定义的 builders,如 JdbcStatementBuilder.withObjectArrayJdbcStatementBuilder.withParamsByPositionJdbcStatementBuilder.withParamsByName,也可以自定义一个 JdbcStatementBuilder 来实现自己的逻辑。
  3. jdbcConnectionOptions:一个 JdbcConnectionOptions 对象,用于设置数据库连接的相关参数,如 URL、用户名、密码等。

完整的示例代码如下:

import org.apache.flink.connector.jdbc.JdbcConnectionOptions;
import org.apache.flink.connector.jdbc.JdbcStatementBuilder;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.connectors.jdbc.JdbcSink;

public class JdbcSinkExample {

    public static void main(String[] args) throws Exception {
        // 创建执行环境
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

        // 创建数据流
        DataStream<MyData> dataStream = ...; // 输入数据流

        // 设置数据库连接参数
        JdbcConnectionOptions connectionOptions = JdbcConnectionOptions.builder()
                .withUrl("jdbc:mysql://localhost:3306/mydb")
                .withUsername("myuser")
                .withPassword("mypassword")
                .build();

        // 创建 JdbcSink
        JdbcSink<MyData> jdbcSink = JdbcSink.sink(
                "INSERT INTO my_table (col1, col2) VALUES (?, ?)",
                (preparedStatement, value) -> {
                    preparedStatement.setString(1, value.getField1());
                    preparedStatement.setInt(2, value.getField2());
                },
                connectionOptions
        );

        // 将 Sink 应用到数据流上
        dataStream.addSink(jdbcSink);

        // 执行程序
        env.execute("JdbcSink Example");
    }

    // 自定义数据类型
    public static class MyData {
        private String field1;
        private int field2;

        // 构造函数、getter 和 setter 方法等

        // ...
    }
}

在上述代码中,我们使用 JdbcSink.sink() 方法创建了一个 JdbcSink 对象,并通过 jdbcStatementBuilder 参数的 lambda 表达式设置了 PreparedStatement 中的占位符参数。在这个例子中,我们将 MyData 数据流的元素写入到名为 my_table 的数据库表中的对应列中。

4.3 Transformation Functions
1、RichFlatMapFunction
package com.test.flink.function.adReport;

import com.test.flink.constant.EnvParameterConfig;
import com.test.flink.model.AdAnalysisReportDoris;
import com.test.flink.model.AdsAggreData;
import lombok.extern.slf4j.Slf4j;
import org.apache.flink.api.common.functions.RichFlatMapFunction;
import org.apache.flink.api.java.utils.ParameterTool;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.util.Collector;

import java.sql.*;

@Slf4j
public abstract class AdReportBaseFunction extends RichFlatMapFunction<AdsAggreData, AdAnalysisReportDoris> {
    protected Connection connection;
    protected PreparedStatement statement;

    protected final ParameterTool argData;
    protected final boolean dayFlag;

    public AdReportBaseFunction(ParameterTool argData, boolean dayFlag) {
        this.argData = argData;
        this.dayFlag = dayFlag;
    }

    @Override
    public void open(Configuration parameters) throws Exception {
        super.open(parameters);
        // 初始化jdbcTemplate对象
//        jdbcTemplate = this.getJdbcTemplate();

        // 在open方法中建立数据库连接和预编译的插入语句
        connection = createConnection();
    }

    @Override
    public void flatMap(AdsAggreData value, Collector<AdAnalysisReportDoris> out) throws Exception {
        getData(value,out, true);
    }

    @Override
    public void close() throws Exception {
        super.close();
        if (statement != null) {
            statement.close();
        }
        if (connection != null) {
            connection.close();
        }
    }

    /**
     * 处理数据
     * @param value
     * @param out
     * @param retry
     */
    abstract void getData(AdsAggreData value, Collector<AdAnalysisReportDoris> out, boolean retry);

    /**
     * 设置从中间表中查询出来的宽表key
     * @param data
     * @param resultSet
     * @throws SQLException
     */
    public void setDataKey(AdAnalysisReportDoris data, ResultSet resultSet) throws SQLException {
        data.setActive_day(resultSet.getInt("active_day"));
        data.setGame_main(resultSet.getInt("game_main"));
        data.setGame_sub(resultSet.getInt("game_sub"));
        data.setMedia_code(resultSet.getInt("media_code"));
        data.setOs(resultSet.getInt("os"));
        data.setChl_main(resultSet.getString("chl_main"));
        data.setChl_sub(resultSet.getString("chl_sub"));
        data.setChl_app(resultSet.getString("chl_app"));
        data.setAd_account_id(resultSet.getString("ad_account_id"));
        data.setAd_campaign_id(resultSet.getString("ad_campaign_id"));
        data.setAd_id(resultSet.getString("ad_id"));
//        data.setAd_external_action(resultSet.getString("ad_external_action"));
//        data.setAd_deep_external_action(resultSet.getString("ad_deep_external_action"));
//        data.setAd_creative_id(resultSet.getString("ad_creative_id"));
        data.setActive_year(resultSet.getInt("active_year"));
        data.setActive_month_num(resultSet.getInt("active_month_num"));
        data.setActive_week_num(resultSet.getInt("active_week_num"));
        data.setActive_hour(resultSet.getInt("active_hour"));
        data.setReg_day(resultSet.getInt("reg_day"));
        data.setReg_hour(resultSet.getInt("reg_hour"));
        data.setReg_year(resultSet.getInt("reg_year"));
        data.setReg_month_num(resultSet.getInt("reg_month_num"));
        data.setReg_week_num(resultSet.getInt("reg_week_num"));
    }


    public Connection createConnection() throws SQLException, ClassNotFoundException {
        // 创建数据库连接
        Class.forName(EnvParameterConfig.DORIS_DRIVER_CLASS_NAME);
        return DriverManager.getConnection(EnvParameterConfig.DORIS_URL, EnvParameterConfig.DORIS_USERNAME, EnvParameterConfig.DORIS_PASSWORD);
    }

    protected void handleActConnectionError(Collector<AdAnalysisReportDoris> out, AdsAggreData value, SQLException e) throws SQLException, ClassNotFoundException {
        // 检查异常信息是否指示连接断开
        if (e.getErrorCode() == 0 && "08S01".equals(e.getSQLState())) {
            // 连接断开,重新连接
            if (connection != null) {
                connection.close();
            }
            connection = createConnection();
            // 重新处理
            getData(value, out, false);
        } else {
            // 其他异常,抛出异常
            throw e;
        }
    }
}

继承

package com.test.flink.function.adReport;

import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.fastjson.JSON;
import com.test.flink.constant.DorisConstants;
import com.test.flink.constant.EnvParameterConfig;
import com.test.flink.model.AdAnalysisReportDoris;
import com.test.flink.model.AdsAggreData;
import com.test.flink.utils.DateUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.flink.api.common.functions.RichFlatMapFunction;
import org.apache.flink.api.java.utils.ParameterTool;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.util.Collector;
import org.springframework.jdbc.core.JdbcTemplate;

import java.io.IOException;
import java.io.InputStream;
import java.sql.*;
import java.util.*;

@Slf4j
public class AdReportKeyFunction extends AdReportBaseFunction {

    public AdReportKeyFunction(ParameterTool argData, boolean dayFlag) {
        super(argData, dayFlag);
    }

    @Override
    public Connection createConnection() throws SQLException, ClassNotFoundException {
        // TODO 创建数据库连接
        Class.forName("ru.yandex.clickhouse.ClickHouseDriver");
        String url = "jdbc:clickhouse://192.168.0.98:8123/dc_dwd_3399?socket_timeout=300000";
        String user = "bduser";
        String password = "bduser";
        return DriverManager.getConnection(url, user, password);
    }

    /**
     * 广告数据分析报表
     * @param value
     * @param retry
     */
    @Override
    void getData(AdsAggreData value, Collector<AdAnalysisReportDoris> out, boolean retry) {
        ResultSet resultSet = null;
        try {
            String sql = EnvParameterConfig.ADS_REPORT_AD_HOUR_HISTORY_SQL;
            if (dayFlag) {
                sql = EnvParameterConfig.ADS_REPORT_AD_DAY_HISTORY_SQL;
            }
            String report_date = DateUtils.dateToString(value.getReport_date(), DateUtils.YYYYMMDD);
            String report_month = DateUtils.dateToString(value.getReport_date(), DateUtils.YYYYMM);
            statement = connection.prepareStatement(sql);
            statement.setInt(1, Integer.parseInt(report_date));
            statement.setInt(2, Integer.parseInt(report_month));
            resultSet = statement.executeQuery();
            while (resultSet.next()) {
                AdAnalysisReportDoris data = new AdAnalysisReportDoris();
                // 设置key
                setDataKey(data, resultSet);
                data.setActive_month(resultSet.getInt("active_month"));
                data.setReg_month(resultSet.getInt("reg_month"));
                data.setOp_tag(DorisConstants.OP_TAG_DELETE);
                data.setOp_version(value.getReport_mills());
                out.collect(data);
            }
        } catch (SQLException throwables) {
            log.error("广告数据分析报表查询历史数据发生异常", throwables);
            if (retry) {
                try {
                    handleActConnectionError(out, value, throwables);
                } catch (SQLException e) {
                    log.error("补偿发生SQLException, day={}", value.getReport_day(), e);
                } catch (ClassNotFoundException classNotFoundException) {
                    log.error("补偿发生ClassNotFoundException, day={}", value.getReport_day(), classNotFoundException);
                }
            }
        } finally {
            if (resultSet != null) {
                try {
                    resultSet.close();
                } catch (SQLException throwables) {
                    log.error("广告数据分析报表查询历史数据关闭[resultSet]发生异常", throwables);
                }
            }
            if (statement != null) {
                try {
                    statement.close();
                } catch (SQLException throwables) {
                    log.error("广告数据分析报表查询历史数据关闭[statement]发生异常", throwables);
                }
            }
        }
    }

}

4.4 Key Selection Functions
1、KeyedProcessFunction

KeyedProcessFunction 是 Apache Flink 中的一个核心函数,在流处理任务中用于对数据流的每个键进行自定义处理操作。它提供了一种灵活的方式来处理流数据,并且可以访问事件时间和处理时间。

以下是 KeyedProcessFunction 的基本结构和用法示例:

public class MyKeyedProcessFunction extends KeyedProcessFunction<KeyType, InputType, OutputType> {
    
    @Override
    public void processElement(InputType value, Context ctx, Collector<OutputType> out) throws Exception {
        // 处理每个输入元素,并根据需要生成输出结果
        // 可以使用 Context 对象访问事件时间、处理时间等信息
        // 可以使用 Collector 发出输出结果
    }
    
    @Override
    public void onTimer(long timestamp, OnTimerContext ctx, Collector<OutputType> out) throws Exception {
        // 定时器触发时执行的逻辑
        // 可以使用 Context 对象访问事件时间、处理时间等信息
        // 可以使用 Collector 发出输出结果
    }
    
    @Override
    public void open(Configuration parameters) throws Exception {
        // 初始化操作,例如建立连接、获取资源等
        // 可选方法,如果需要可以重写
    }
    
    @Override
    public void close() throws Exception {
        // 清理操作,例如释放资源、关闭连接等
        // 可选方法,如果需要可以重写
    }
}

上述代码是一个自定义的 KeyedProcessFunction 类的示例,其中 KeyType 是键的类型,InputType 是输入元素的类型,OutputType 是输出元素的类型。您可以根据具体的业务逻辑来编写 processElement 方法和 onTimer 方法,在这些方法中定义对数据流的处理操作。

processElement 方法中,您可以根据需要处理每个输入元素,并使用 Context 对象访问事件时间、处理时间等信息,使用 Collector 对象发出输出结果。

onTimer 方法用于处理定时器触发的逻辑,您可以在此方法中执行定时任务。同样可以使用 Context 对象和 Collector 对象来访问信息和发送输出结果。

open 方法在 KeyedProcessFunction 实例初始化时调用,可以进行一些初始化操作,例如建立连接、获取资源等。

close 方法在 KeyedProcessFunction 实例关闭时调用,可以进行清理操作,例如释放资源、关闭连接等。

使用 KeyedProcessFunction,您可以灵活地处理流数据,并根据键值实现自定义的逻辑。它是 Flink 中十分强大和常用的函数之一。

应用:

package com.test.flink.function.adReport;

import com.test.flink.model.AdsAggreData;
import lombok.extern.slf4j.Slf4j;
import org.apache.flink.api.common.state.ValueState;
import org.apache.flink.api.common.state.ValueStateDescriptor;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
import org.apache.flink.util.Collector;
import org.apache.flink.util.OutputTag;

@Slf4j
public class AdReportDelayProcessFunction extends KeyedProcessFunction<String, AdsAggreData, AdsAggreData> {

    private ValueState<Boolean> delayState;
    private ValueState<AdsAggreData> eventState;

    @Override
    public void open(Configuration parameters) throws Exception {
        super.open(parameters);
        // 初始化状态
        ValueStateDescriptor<Boolean> descriptor = new ValueStateDescriptor<>("delayState", Boolean.class);
        delayState = getRuntimeContext().getState(descriptor);

        ValueStateDescriptor<AdsAggreData> eventStateDescriptor = new ValueStateDescriptor<>("eventState", AdsAggreData.class);
        eventState = getRuntimeContext().getState(eventStateDescriptor);
    }

    @Override
    public void processElement(AdsAggreData value, Context ctx, Collector<AdsAggreData> out) throws Exception {
        if (delayState.value() == null) {
            // 设置定时器,延迟60秒处理
            long timerTimestamp = ctx.timerService().currentProcessingTime() + 60000;
            ctx.timerService().registerProcessingTimeTimer(timerTimestamp);

            // 标记定时器已设置
            delayState.update(true);
        }
        // 保存需要延迟处理的事件
        eventState.update(value);
    }

    @Override
    public void onTimer(long timestamp, OnTimerContext ctx, Collector<AdsAggreData> out) throws Exception {
        // 在定时器触发时执行延迟处理的逻辑
        // 获取需要延迟处理的事件
        AdsAggreData event = eventState.value();
        if (event != null) {
            // 执行具体的延迟处理逻辑
            log.info("timestamp={}, Delayed event processing: {}", timestamp, event);
            out.collect(event);
            // 清空保存的事件状态
            eventState.clear();
            delayState.clear();
        }
    }
}

4.5 Aggregation Functions
4.6 Process Functions
4.7 Sink Functions
1、RichSinkFunction

RichSinkFunction是Apache Flink中用于自定义数据下沉(sink)操作的一个抽象类。它是SinkFunction接口的一个具体实现,提供了更多的功能和生命周期方法。

RichSinkFunction中定义了以下几个常用的方法:

  1. open():在sink任务启动时调用。你可以在这个方法中初始化连接、打开资源等操作。
  2. invoke():对每个输入元素调用一次。你可以在这个方法中执行数据下沉的逻辑。
  3. close():在sink任务结束时调用。你可以在这个方法中关闭连接、释放资源等操作。

除了上述方法,RichSinkFunction还提供了一些其他方法,如:

  • getRuntimeContext():获取运行时上下文对象,可以获得一些关于任务执行环境的信息,例如并行度、子任务索引等。

使用RichSinkFunction需要继承该类并实现invoke()方法,根据具体的需求编写相应的数据下沉逻辑。举例来说,你可以将数据写入数据库、发送到消息队列、输出到文件等。

下面是一个简单的示例,演示如何使用RichSinkFunction将数据写入控制台:

import org.apache.flink.streaming.api.functions.sink.RichSinkFunction;

public class ConsoleSink extends RichSinkFunction<String> {

    @Override
    public void invoke(String value, Context context) throws Exception {
        System.out.println(value); // 在控制台打印数据
    }

    @Override
    public void open(Configuration parameters) throws Exception {
        // 在这里进行初始化操作,例如建立数据库连接等
    }

    @Override
    public void close() throws Exception {
        // 在这里进行资源释放操作,例如关闭数据库连接等
    }
}

在上述示例中,我们创建了一个名为 ConsoleSink 的类,继承自 RichSinkFunction。在 invoke() 方法中,我们将输入的字符串值打印到控制台;在 open() 方法中,可以进行一些初始化操作;在 close() 方法中,可以进行一些资源释放操作。

请注意,使用RichSinkFunction时,需要将数据类型作为泛型参数传递给基类。在上述示例中,我们将String类型作为泛型参数传递给RichSinkFunction。根据实际情况,你可以将具体的数据类型作为泛型参数传递给RichSinkFunction

4.8 AsyncFunction
1、RichAsyncFunction

RichAsyncFunction 是 Flink 中的一个接口,它继承自 AsyncFunction 接口,并添加了一些生命周期方法和上下文信息,可以用于在异步流处理中访问和管理一些全局状态或资源。

下面是一个示例,展示如何使用 RichAsyncFunction 来实现一个异步函数:

import org.apache.flink.streaming.api.functions.async.RichAsyncFunction;
import org.apache.flink.util.Collector;

public class MyAsyncFunction extends RichAsyncFunction<String, Integer> {
    
    @Override
    public void open(Configuration parameters) throws Exception {
        // 在这里可以进行初始化操作,比如创建数据库连接池、获取全局配置等
        // 这个方法在任务开始时会被调用一次
        super.open(parameters);
        // 初始化操作...
    }

    @Override
    public void close() throws Exception {
        // 在这里可以进行资源释放操作,比如关闭数据库连接池、清理全局状态等
        // 这个方法在任务结束时会被调用一次
        super.close();
        // 资源释放操作...
    }

    @Override
    public void asyncInvoke(String input, Collector<Integer> collector) throws Exception {
        // 在这里编写异步逻辑,比如与外部系统的异步调用、数据库查询等
        // 异步操作完成后,将结果通过 `collector` 发射出去

        // 模拟异步处理,延迟 1 秒
        Thread.sleep(1000);

        // 发射异步处理结果
        collector.collect(Integer.parseInt(input));
    }
}

在上述示例中,我们创建了一个名为 MyAsyncFunction 的类,它实现了 RichAsyncFunction 接口,并重写了其中的几个方法。

  • open() 方法中,我们可以进行一些初始化操作,例如创建数据库连接池或加载全局配置。这个方法在任务开始时会被调用一次。
  • close() 方法中,我们可以进行资源释放操作,例如关闭数据库连接池或清理全局状态。这个方法在任务结束时会被调用一次。
  • asyncInvoke() 方法中,我们编写异步逻辑,例如与外部系统的异步调用或数据库查询。在这里,我们模拟了一个延迟 1 秒的异步处理过程,并通过 collector 发射异步处理的结果。

通过使用 RichAsyncFunction,我们可以在异步函数中访问和管理全局状态或资源,提供更灵活的异步处理能力。

要使用 RichAsyncFunction,只需将其作为异步函数的实现类,并根据需求重写生命周期方法和异步处理方法即可。

4.9 WindowFunction
1、ProcessWindowFunction

ProcessWindowFunction 是 Apache Flink 流处理框架中的一个函数接口,用于对窗口中的元素进行处理。它是 WindowFunction 接口的子接口,提供了更为灵活的处理能力。

常见的 ProcessWindowFunction 方法是 process(),它接收以下参数:

  • key:窗口内元素的键值。
  • context:提供有关当前窗口和流程的上下文信息,如当前处理时间、窗口状态等。
  • elements:窗口内的元素 Iterable。
  • out:输出结果的 Collector。

使用 ProcessWindowFunction 可以实现更复杂的窗口计算逻辑,例如在窗口关闭时调整窗口的大小或触发一些特殊操作。

下面是一个示例代码片段,演示了如何使用 ProcessWindowFunction 来计算一个窗口中所有元素的总和:

DataStream<Tuple2<String, Integer>> dataStream = ...; // 输入数据流

dataStream.keyBy(0)
    .window(TumblingEventTimeWindows.of(Time.seconds(5)))
    .process(new SumProcessWindowFunction())
    .print();

public class SumProcessWindowFunction extends ProcessWindowFunction<Tuple2<String, Integer>, String, String, TimeWindow> {
    @Override
    public void process(String key, Context context, Iterable<Tuple2<String, Integer>> elements, Collector<String> out) throws Exception {
        int sum = 0;
        for (Tuple2<String, Integer> element : elements) {
            sum += element.f1;
        }
        out.collect("Key: " + key + ", Sum: " + sum);
    }
}

在上述示例中,首先按键进行分组(keyBy()),然后使用滚动时间窗口(window())将数据流划分为 5 秒的窗口。然后,在 SumProcessWindowFunction 中实现了对窗口内元素求和的逻辑。最后,通过 print() 方法将结果打印出来。

需要注意的是,使用 ProcessWindowFunction 需要导入相应的依赖包,并确保你的 Flink 版本支持该函数接口。

5 操作案例

5.1 注册kafka源的Table
  public static void createTmpTable(StreamTableEnvironment tableEnv, String tableName, List<String> fieldList, String topics, Properties consumeProperties) {
        // 临时表
        StringBuilder sbTmp = new StringBuilder();
        sbTmp.append("  CREATE TABLE TMP_").append(tableName).append(" ( ");

        // 字段
        int size = fieldList.size();
        for (int i = 0; i < size - 1; i++) {
            sbTmp.append("  ").append(fieldList.get(i)).append(", ");
        }
        sbTmp.append("  ").append(fieldList.get(size - 1)).append(" ");

        sbTmp.append("  ) WITH (");
        sbTmp.append("  'connector' = 'kafka',");
        sbTmp.append("  'topic' = '").append(String.join(";", topics.split(","))).append("',");
        sbTmp.append("  'properties.group.id' = '").append(consumeProperties.getProperty("group.id") + "-" + tableName).append("',");
        sbTmp.append("  'properties.bootstrap.servers' = '").append(consumeProperties.getProperty("bootstrap.servers")).append("',");
        sbTmp.append("  'scan.startup.mode' = 'earliest-offset',");
        sbTmp.append("  'format' = 'json',");
        sbTmp.append("  'json.fail-on-missing-field' = 'false',");
        sbTmp.append("  'json.ignore-parse-errors' = 'true'");
        sbTmp.append("  )");

        tableEnv.executeSql(sbTmp.toString());
    }
5.2 注册表视图
    public static void createTable(StreamTableEnvironment tableEnv, String tableName, String primaryKeys) {
        StringBuilder sb = new StringBuilder();
        sb.append("  CREATE VIEW ").append(tableName).append(" AS ");
        sb.append("  SELECT * ");
        sb.append("  FROM ( ");
        sb.append("      SELECT *, ROW_NUMBER() OVER (PARTITION BY ").append(primaryKeys).append(" ORDER BY op_version DESC) AS row_num ");
        sb.append("      FROM TMP_").append(tableName).append(" ");
        sb.append("  ) WHERE row_num = 1; ");

        // 去重合并表
        tableEnv.executeSql(sb.toString());
    }

6 部署

6.1 打包
E:\dataCenter\dongxinCode\dx-dw-1.0> mvn package
6.2 程序部署

flink run 是 Apache Flink 的命令行工具,用于提交和运行 Flink 作业。在 -c 参数后面,你需要指定 Flink 作业的主类名。

例如,假设你的 Flink 作业的主类是 com.example.MyJob,你可以使用以下命令来提交和运行该作业:

Copy Codeflink run -d -c com.example.MyJob my-job.jar

在上述命令中,-d 参数表示以后台模式运行作业,即作业会在后台执行而不会阻塞当前终端。my-job.jar 是打包好的 Flink 作业 JAR 文件。

请确保在运行命令之前已经正确设置了 Flink 环境,并且作业的 JAR 文件已经准备好。此外,还需要替换 com.example.MyJob 为实际的 Flink 作业主类名。

示例:

cd /data/project/dx-dw/
flink run -d -c com.dogame.dx.dw.game.job.RegJob ./dx-dw-game-1.0-SNAPSHOT-jar-with-dependencies.jar  dev 
6.3 Flink管理界面

todo

Flink问题

1、KafkaDeserializationSchema 和 KafkaRecordDeserializationSchema有何区别

KafkaDeserializationSchemaKafkaRecordDeserializationSchema 的主要区别在于它们处理 Kafka 消息的方式和提供的功能。

  1. 数据类型:KafkaDeserializationSchema 是以消息为单位进行反序列化的,而 KafkaRecordDeserializationSchema 是以记录(record)为单位进行反序列化的。
  2. 接口方法:KafkaDeserializationSchema 通过 deserialize() 方法将 Kafka 消息反序列化为指定类型的对象,而 KafkaRecordDeserializationSchema 则通过 deserialize() 方法将 Kafka 记录反序列化为对象。
  3. 数据源和消费者:KafkaDeserializationSchema 通常用于与 Flink 中的 FlinkKafkaConsumer 配合使用,用于从 Kafka 主题消费数据。KafkaRecordDeserializationSchema 则可与更底层的 Kafka 消费者 API 进行集成,以直接访问 Kafka 记录,并将其转换为对象。
  4. 细粒度控制:由于 KafkaRecordDeserializationSchema 以记录为单位进行反序列化,可以对每个记录进行更细粒度的控制。例如,您可以在 KafkaRecordDeserializationSchema 中实现 isEndOfStream() 方法,以确定是否已经到达 Kafka 主题的末尾。

总体而言,KafkaDeserializationSchema 更适合常规的使用场景,其中消息是基本单位。而 KafkaRecordDeserializationSchema 则提供了更多的灵活性和控制权,适用于需要直接操作 Kafka 记录的特殊情况。

要选择使用哪种接口,您可以根据您的具体需求和场景来决定。如果您只需要对 Kafka 消息进行基本的反序列化处理,通常选择 KafkaDeserializationSchema 即可。如果需要更底层的控制和访问能力,以及对每个记录进行操作的灵活性,则可以考虑使用 KafkaRecordDeserializationSchema

2、DataStream和DataStreamSource有何区别

DataStreamDataStreamSource 是 Apache Flink 中用于处理流式数据的概念和组件,它们之间有以下区别:

  1. 定义:
    • DataStream: DataStream 是代表一条或多条无界数据记录流的概念。它表示无界数据流的抽象,并提供了一系列操作和转换方法,用于对数据流进行处理和分析。
    • DataStreamSource: DataStreamSource 是用于从外部数据源(例如文件、Kafka 主题、Socket)读取数据并创建 DataStream 的组件。它是 DataStream 的来源,负责将外部数据源的数据读取并转换为 DataStream
  2. 角色和功能:
    • DataStream:作为数据流的抽象,DataStream 提供了类似于批处理的操作,如 mapfilterreduce 等,以及窗口操作、状态管理、时间特性等功能。DataStream 是进行复杂流处理的核心概念。
    • DataStreamSourceDataStreamSource 负责从外部数据源读取数据,例如读取文件中的行、从 Kafka 主题消费消息等。它负责数据的输入,并将其转换为 DataStream 供后续处理。
  3. 创建方式:
    • DataStream 可以通过对其他 DataStream 进行转换操作生成,也可以通过连接到外部数据源的 DataStreamSource 创建。
    • DataStreamSourceDataStream 的源头,通过不同的方法(如 DataStreamSource.fromElements()DataStreamSource.fromCollection()DataStreamSource.fromSocketTextStream() 等)来创建具体类型的 DataStreamSource
  4. 用途:
    • DataStream 用于处理和分析无界的实时数据流。它可以执行实时计算、复杂事件处理、流处理等任务。
    • DataStreamSource 是用于将外部数据源的数据读取和转换为 DataStream 的组件。它是数据流的入口点,用于定义数据源。

总之,DataStream 是 Flink 中对无界数据记录流的抽象,提供了丰富的操作和功能。而 DataStreamSource 则用于从外部数据源读取数据并创建 DataStream,作为数据流的来源。两者是互相关联的,DataStreamSource 负责数据的输入,而 DataStream 进行复杂的流式处理和分析。

3、StreamExecutionEnvironment的addSource和 fromSource有何区别

StreamExecutionEnvironment 类提供了两种方法来从自定义数据源创建数据流:addSource()fromSource()。它们之间的区别如下:

  1. addSource(SourceFunction<T> sourceFunction): addSource() 方法是一个简便的方法,用于将实现了 SourceFunction 接口的自定义数据源添加到流处理环境,并返回相应类型的数据流。SourceFunction 接口需要用户手动实现 run()cancel() 方法来定义从数据源产生数据和停止条件。addSource() 方法会自动将 SourceFunction 转化为一个算子,并将其添加到作业图中。

示例:

streamExecutionEnvironment.addSource(new MySourceFunction());
  1. fromSource(SourceFunction<T> sourceFunction, WatermarkStrategy<T> watermarkStrategy): fromSource() 方法是一个更灵活的方法,提供了额外的参数来定义数据源和水位线策略。除了实现 SourceFunction 接口外,还可以通过 fromSource() 方法传递一个 WatermarkStrategy 参数来定义水位线的生成方式。WatermarkStrategy 可以通过时间戳、周期性生成或任何其他自定义逻辑来生成水位线。

示例:

WatermarkStrategy<MyEvent> watermarkStrategy = WatermarkStrategy
    .<MyEvent>forBoundedOutOfOrderness(Duration.ofSeconds(10))
    .withTimestampAssigner((event, timestamp) -> event.getTimestamp());

streamExecutionEnvironment.fromSource(new MySourceFunction(), watermarkStrategy);

使用 fromSource() 方法可以更加灵活地定义数据源和水位线策略,但相应地需要更多的编码工作和配置。

综上所述,addSource() 是一个简易的方法来将自定义数据源添加到流处理环境中,而 fromSource() 则提供了更多的灵活性,可以额外传递水位线策略来定义水位线生成的方式。选择使用哪种方法取决于具体需求和场景的复杂程度。

4、TableResult和Table有何区别

在 Flink 中,有两个表示查询结果的 API:Table 和 TableResult。它们之间有一些区别,如下所述。

  1. Table:
    • Table 是 Flink SQL 和 Table API 的核心概念之一,它表示一个逻辑表格。
    • Table 是一个定义了结构和模式的抽象概念,可以通过 SQL 或 Table API 进行查询、转换和操作。
    • Table 可以作为输入或输出传递给各种 Flink 的运行时组件和连接器,如批处理作业、流式作业、ETL 管道等。
  2. TableResult:
    • TableResult 是在 Flink 1.11 版本中引入的新的 API 类型,用于表示查询执行的结果。
    • TableResult 包含了查询的元数据和实际数据结果。
    • 它提供了一些方法来检索和处理查询结果,如迭代处理数据、转换为 DataStream 或 DataSet 等。

总结来说,Table 是一个逻辑表格的抽象,用于定义结构、模式和进行查询操作;而 TableResult 是用于表示具体查询执行结果的 API 类型,包含查询的元数据和数据结果,并提供了一些操作方法。

值得注意的是,TableResult 在 Flink 1.11 版本中是作为一个更高级别的 API 类型引入的,旨在简化处理查询结果的操作。如果你使用较低版本的 Flink,可能会使用其他 API 或方法来处理查询结果。

;