
本篇分享数据库主从方案,案例采用springboot+mysql+mybatis演示;要想在代码中做主从选择,通常需要明白什么时候切换数据源,怎么切换数据源,下面以代码示例来做阐述; 搭建测试环境(1个master库2个slave库) DataSource多数据源配置 设置mybatis数据源 拦截器+注解设置master和slave库选择 选出当前请求要使用的slave从库 测试用例 搭建测试环境(1个master库2个slave库) 由于测试资源优先在本地模拟创建3个数据库,分别是1个master库2个slave库,里面分别都有一个tblArticle表,内容也大致相同(为了演示主从效果,我把从库中表的title列值增加了slave字样):再来创建一个db.properties,分别配置3个数据源,格式如下: spring.datasource0.jdbc-url=jdbc:mysql://localhost:3306/db0?useUnicode=true&characterEncoding=utf-8&useSSL=false spring.datasource0.username=root spring.datasource0.password=123456 spring.datasource0.driver-class-name=com.mysql.jdbc.Driver spring.datasource1.jdbc-url=jdbc:mysql://localhost:3306/db1?useUnicode=true&characterEncoding=utf-8&useSSL=false spring.datasource1.username=root spring.datasource1.password=123456 spring.datasource1.driver-class-name=com.mysql.jdbc.Driver spring.datasource2.jdbc-url=jdbc:mysql://localhost:3306/db2?useUnicode=true&characterEncoding=utf-8&useSSL=false spring.datasource2.username=root spring.datasource2.password=123456 spring.datasource2.driver-class-name=com.mysql.jdbc.Driver 同时我们创建具有对应关系的DbType枚举,帮助我们使代码更已读: public class DbEmHelper { public enum DbTypeEm { db0(0, "db0(默认master)", -1), db1(1, "db1", 0), db2(2, "db2", 1); /** * 用于筛选从库 * * @param slaveNum 从库顺序编号 0开始 * @return */ public static Optional<DbTypeEm> getDbTypeBySlaveNum(int slaveNum) { return Arrays.stream(DbTypeEm.values()).filter(b -> b.getSlaveNum() == slaveNum).findFirst(); } DbTypeEm(int code, String des, int slaveNum) { this.code = code; this.des = des; this.slaveNum = slaveNum; } private int code; private String des; private int slaveNum; //get,set省略 } } DataSource多数据源配置 使用上面3个库连接串信息,配置3个不同的DataSource实例,达到多个DataSource目的;由于在代码中库的实例需要动态选择,因此我们利用AbstractRoutingDataSource来聚合多个数据源;下面是生成多个DataSource代码: @Configuration public class DbConfig { @Bean(name = "dbRouting") public DataSource dbRouting() throws IOException { //加载db配置文件 InputStream in = this.getClass().getClassLoader().getResourceAsStream("db.properties"); Properties pp = new Properties(); pp.load(in); //创建每个库的datasource Map<Object, Object> targetDataSources = new HashMap<>(DbEmHelper.DbTypeEm.values().length); Arrays.stream(DbEmHelper.DbTypeEm.values()).forEach(dbTypeEm -> { targetDataSources.put(dbTypeEm, getDataSource(pp, dbTypeEm)); }); //设置多数据源 DbRouting dbRouting = new DbRouting(); dbRouting.setTargetDataSources(targetDataSources); return dbRouting; } /** * 创建库的datasource * * @param pp * @param dbTypeEm * @return */ private DataSource getDataSource(Properties pp, DbEmHelper.DbTypeEm dbTypeEm) { DataSourceBuilder<?> builder = DataSourceBuilder.create(); builder.driverClassName(pp.getProperty(JsonUtil.formatMsg("spring.datasource{}.driver-class-name", dbTypeEm.getCode()))); builder.url(pp.getProperty(JsonUtil.formatMsg("spring.datasource{}.jdbc-url", dbTypeEm.getCode()))); builder.username(pp.getProperty(JsonUtil.formatMsg("spring.datasource{}.username", dbTypeEm.getCode()))); builder.password(pp.getProperty(JsonUtil.formatMsg("spring.datasource{}.password", dbTypeEm.getCode()))); return builder.build(); } } 能够看到一个DbRouting实例,其是继承了AbstractRoutingDataSource,她里面有个Map变量来存储多个数据源信息: public class DbRouting extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey() { return DbContextHolder.getDb().orElse(DbEmHelper.DbTypeEm.db0); } } DbRouting里面主要重写了determineCurrentLookupKey(),通过设置和存储DataSource集合的Map相同的key,以此达到选择不同DataSource的目的,这里使用ThreadLocal获取同一线程存储的key;主要看AbstractRoutingDataSource类中下面代码: protected DataSource determineTargetDataSource() { Assert.notNull(this.resolvedDataSources, "DataSource router not initialized"); Object lookupKey = this.determineCurrentLookupKey(); DataSource dataSource = (DataSource)this.resolvedDataSources.get(lookupKey); if(dataSource == null && (this.lenientFallback || lookupKey == null)) { dataSource = this.resolvedDefaultDataSource; } if(dataSource == null) { throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]"); } else { return dataSource; } } 设置mybatis数据源 本次演示为了便利,这里使用mybatis的注解方式来查询数据库,我们需要给mybatis设置数据源,我们可以从上面的声明DataSource的bean方法获取: @EnableTransactionManagement @Configuration public class MybaitisConfig { @Resource(name = "dbRouting") DataSource dataSource; @Bean public SqlSessionFactory sqlSessionFactory() throws Exception { SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean(); factoryBean.setDataSource(dataSource); // factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:*")); return factoryBean.getObject(); } } 我们使用的mybatis注解方式来查询数据库,所以不需要加载mapper的xml文件,下面注解方式查询sql: @Mapper public interface ArticleMapper { @Select("select * from tblArticle where id = #{id}") Article selectById(int id); } 拦截器+注解来选择master和slave库 通常操作数据的业务逻辑都放在service层,我们希望service中不同方法使用不同的库;比如:添加、修改、删除、部分查询方法等,使用master主库来操作,而大部分查询操作可以使用slave库来查询;这里通过拦截器+灵活的自定义注解来实现我们的需求: @Documented @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface DbType { boolean isMaster() default true; } 注解参数默认选择master库来操作业务(看具体需求吧) @Aspect @Component public class DbInterceptor { //全部service层请求都走这里,ThreadLocal才能有DbType值 private final String pointcut = "execution(* com.sm.service..*.*(..))"; @Pointcut(value = pointcut) public void dbType() { } @Before("dbType()") void before(JoinPoint joinPoint) { System.out.println("before..."); MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); Method method = methodSignature.getMethod(); DbType dbType = method.getAnnotation(DbType.class); //设置Db DbContextHolder.setDb(dbType == null ? false : dbType.isMaster()); } @After("dbType()") void after() { System.out.println("after..."); DbContextHolder.remove(); } } 拦截器拦截service层的所有方法,然后获取带有自定义注解DbType的方法的isMaster值,DbContextHolder.setDb()方法判断走master还是slave库,并赋值给ThreadLocal: public class DbContextHolder { private static final ThreadLocal<Optional<DbEmHelper.DbTypeEm>> dbTypeEmThreadLocal = new ThreadLocal<>(); private static final AtomicInteger atoCounter = new AtomicInteger(0); public static void setDb(DbEmHelper.DbTypeEm dbTypeEm) { dbTypeEmThreadLocal.set(Optional.ofNullable(dbTypeEm)); } public static Optional<DbEmHelper.DbTypeEm> getDb() { return dbTypeEmThreadLocal.get(); } public static void remove() { dbTypeEmThreadLocal.remove(); } /** * 设置主从库 * * @param isMaster */ public static void setDb(boolean isMaster) { if (isMaster) { //主库 setDb(DbEmHelper.DbTypeEm.db0); } else { //从库 setSlave(); } } private static void setSlave() { //累加值达到最大时,重置 if (atoCounter.get() >= 100000) { atoCounter.set(0); } //排除master,选出当前线程请求要使用的db从库 - 从库算法 int slaveNum = atoCounter.getAndIncrement() % (DbEmHelper.DbTypeEm.values().length - 1); Optional<DbEmHelper.DbTypeEm> dbTypeEm = DbEmHelper.DbTypeEm.getDbTypeBySlaveNum(slaveNum); if (dbTypeEm.isPresent()) { setDb(dbTypeEm.get()); } else { throw new IllegalArgumentException("从库未匹配"); } } } 这一步骤很重要,通过拦截器来到达选择master和slave目的,当然也有其他方式的; 选出当前请求要使用的slave从库 上面能选择出master和slave走向了,但是往往slave至少有两个库存在;我们需要知道怎么来选择多个slave库,目前最常用的方式通过计数器取余的方式来选择: private static void setSlave() { //累加值达到最大时,重置 if (atoCounter.get() >= 100000) { atoCounter.set(0); } //排除master,选出当前线程请求要使用的db从库 - 从库算法 int slaveNum = atoCounter.getAndIncrement() % (DbEmHelper.DbTypeEm.values().length - 1); Optional<DbEmHelper.DbTypeEm> dbTypeEm = DbEmHelper.DbTypeEm.getDbTypeBySlaveNum(slaveNum); if (dbTypeEm.isPresent()) { setDb(dbTypeEm.get()); } else { throw new IllegalArgumentException("从库未匹配"); } } 这里根据余数来匹配对应DbType枚举,选出DataSource的Map需要的key,并且赋值到当前线程ThreadLocal中; /** * 用于筛选从库 * * @param slaveNum 从库顺序编号 0开始 * @return */ public static Optional<DbTypeEm> getDbTypeBySlaveNum(int slaveNum) { return Arrays.stream(DbTypeEm.values()).filter(b -> b.getSlaveNum() == slaveNum).findFirst(); } 测试用例 完成上面操作后,我们搭建个测试例子,ArticleService中分别如下3个方法,不同点在于@DbType注解的标记: @Service public class ArticleService { @Autowired ArticleMapper articleMapper; @DbType public Article selectById01(int id) { Article article = articleMapper.selectById(id); System.out.println(JsonUtil.formatMsg("selectById01:{} --- title:{}", DbContextHolder.getDb().get(), article.getTitle())); return article; } @DbType(isMaster = false) public Article selectById02(int id) { Article article = articleMapper.selectById(id); System.out.println(JsonUtil.formatMsg("selectById02:{} --- title:{}", DbContextHolder.getDb().get(), article.getTitle())); return article; } public Article selectById(int id) { Article article = articleMapper.selectById(id); System.out.println(JsonUtil.formatMsg("selectById:{} --- title:{}", DbContextHolder.getDb().get(), article.getTitle())); return article; } } 在同一个Controller层接口方法中去调用这3个service层方法,按照正常逻辑来讲,不出意外得到的结果是这样:请求了两次接口,得到结果是:selectById01方法:标记了@DbType,但默认走isMaster=true,实际走了db0(master)库selectById02方法:标记了@DbType(isMaster = false),实际走了db1(slave1)库selectById方法:没有标记了@DbType,实际走了db2(slave2)库,因为拦截器中没有找到DbType注解,让其走了slave方法;因为selectById02执行过一次slave方法,计数器+1了,因此余数也变了所以定位到了slave2库(如果是基数调用,selectById02和selectById方法来回切换走不同slave库);
本篇分享的内容是在相同类中方法间调用时Aop失效处理方案,该问题我看有很多文章描述了,不过大多是从事务角度分享的,本篇打算从日志aop方面分享(当然都是aop,失效和处理方案都是一样),以下都是基于springboot演示; 快速定义个日志Appender 快速定义个拦截器和日志注解(aop) 模拟相同类中方法间调用时aop失效 Aop失效处理方案(就两种足够了) 快速定义个日志Appender 日志我还是喜欢log4j,大部分朋友也同样吧,这里lombok与log4j结合来完成我们的日志,如下maven包(最新mvn还是建议去官网找): <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>2.0.0-alpha0</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> <version>2.0.0-alpha0</version> </dependency> 先继承log4j的AppenderSkeleton重写下append方法,简单记录下就行,如下: public class MyLogAppend extends AppenderSkeleton { private String author; public void setAuthor(String author) { this.author = author; } @Override protected void append(LoggingEvent loggingEvent) { System.out.println( JsonUtil.formatMsg("date -- {},level -- {},message -- {}", LocalDate.now(), loggingEvent.getLevel(), loggingEvent.getMessage())); } @Override public void activateOptions() { super.activateOptions(); System.out.println("author:" + this.author); } @Override public void close() { this.closed = true; } @Override public boolean requiresLayout() { return false; } } 然后项目根目录增加log4j.properties配置文件,配置内容定义info级别,就此完成了log4j自定义记录日志了: log4j.rootLogger=info,MyLogAppend log4j.appender.MyLogAppend=com.sm.component.log.MyLogAppend log4j.appender.MyLogAppend.author=shenniu003 快速定义个拦截器和日志注解(aop) 通常同类中不同方法调用是常事,可以直接用this.xx();有时有这样需求,需要各个调用方法时候的参数记录下来,因此我们需要个拦截器,再增加个自定义注解方便使用: @Aspect @Component @Slf4j public class MyLogInterceptor { private final String pointcut = "@annotation(com.sm.component.ServiceLog)"; @Pointcut(pointcut) public void log() { } @Before(value = "log()") void before(JoinPoint joinPoint) { Signature signature = joinPoint.getSignature(); log.info( JsonUtil.formatMsg("method:{},params:{}", signature.toLongString(), joinPoint.getArgs())); } } @Documented @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface ServiceLog { } 拦截器拦截带有@ServiceLog注解的方法,然后记录请求参数和方法名 模拟相同类中方法间调用时aop失效 利用上面完成的日志注解,这里在OrderService类中用getOrderDetail方法去调用getOrderLog方法,他两都标记日志注解便于记录参数日志;同时getOrderDetail方法也调用另外一个UserService类中的getNickName方法,便于比较: @Service public class OrderService { @Autowired UserService userService; @ServiceLog public String getOrderDetail(String orderNum) { String des = "订单号【" + orderNum + "】月饼一盒"; userService.getNickName(orderNum); this.getOrderLog(orderNum + "11111"); return des; } @ServiceLog public List<String> getOrderLog(String orderNum) { List<String> logs = new ArrayList<>(); IntStream.range(0, 5).forEach(b -> { logs.add("用户" + b + "购买成功"); }); return logs; } } @Service public class UserService { @ServiceLog public String getNickName(String userId) { return "神牛" + userId; } } 方法调用重点截图:然后运行程序,接口触发调用getOrderDetail方法,以下拦截器中记录的日志信息:能够看出拦截器只记录到了getOrderDetail和getNickName方法的日志,因此可以肯定getOrderLog根本没有走拦截器,尽管在方法上加了日志@ServiceLog注解也没用。 Aop失效处理方案(就两种足够了) 就上面相同类中方法间调用拦截器(aop)没起作用,我们有如下常用两种方式处理方案; 用@Autowired或Resource引入自身依赖 开启暴露代理类,AopContext.currentProxy()方式获取代理类 第一种:主要使用注解方法引入自身代理依赖,不要使用构造的方式会有循环依赖问题,以下使用方式:第二种:通过暴露代理类方式,实际原理是把代理类添加到当前请求的ThreadLocal里面,然后在使用时从ThreadLocal中获取代理类,再调用对应的方法,开启方式需要: @EnableAspectJAutoProxy(exposeProxy = true) 然后方法中如下使用即可:最后来看下使用这两种方式正常走拦截器效果: 不管是日志拦截器或事务,他们都是aop的方式,底层原理走的代理方式,只有使用代理类才会正常执行拦截器,而this.xxx()使用的是自身实例对象,因此会出现上面失效的情况。
本篇分享内容是关于生成分布式Id的其中之一方案,除了redis方案之外还有如:数据库,雪花算法,mogodb(object_id也是数据库)等方案,对于redis来说是我们常用并接触比较多的,因此主要谈谈结合redis生成分布式id方案。 分布式Id设计流程图 基于redis的hash自动increment累加生成有序Id 定期删除无用hash列 分布式Id设计流程图(有点粗略) 基于redis的hash自动increment累加生成有序Id 使用redis方案生成id,其中之一的方式主要使用increment(递增),不管是string、hash等都具有该方法,为了更方便管理我们id生成key这里建议使用hash的列的方式,以下内容都基于springboot分享; 当然,第一步我们需要创建一个hash和hkey才行,至于在业务第一次被访问来创建这个hash还是通过服务自动创建这个看业务和流量,这里的hkey是有一定规则的(当然不用局限性),这里我按照日期格式来做key,可以有如下代码: /** * 生成每天的初始Id * @param hashName * @return */ public String initPrimaryId(String hashName) { Assert.hasLength(hashName, "hashName不能为空"); String hashCol = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")); //自定义编号规则 String hashColVal = hashCol + "00001"; redisTemplate.opsForHash().putIfAbsent(hashName, hashCol, hashColVal); return hashCol; } 上面很容易理解,hash中key是有每天日期格式组成,意思每天都需要生成一个新的日期key,通过putIfAbsent达到不重复添加的原则,至于hval可以根据自定义编号规则来生成一串数字字符(注:一定要数字) 有了上面的基础,我们仅仅需要increment来累加,redis即帮我们完整hval+1的操作,当然可以自定义累加数,如下代码: /** * 获取分布式Id * * @param hashName * @return */ public long getPrimaryId(String hashName) { try { String hashCol = initPrimaryId(hashName); return redisTemplate.opsForHash().increment(hashName, hashCol, 1); } catch (Exception ex) { ex.printStackTrace(); } return 0; } 定期删除无用hash列 就上面我们通过hash来设置每天id只增初始值,hash的hkey布局用自动过期功能,因此我们需要代码中维护一套清除来hkey的机制,既然id是根据日期生成,我们可以就用往前推n天的方式达到清除老hkey目的: /** * 删除多少天之前的cols * @param hashName * @param lessDay * @return */ public Long removePrimaryByLessDay(String hashName, int lessDay) { try { //当前日期 String hashCol = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")); long idl = Long.valueOf(hashCol) - lessDay; String[] removeCols = redisTemplate.opsForHash().entries(hashName).keySet().stream(). map(key -> key.toString()). filter(key -> idl > Long.valueOf(key)). //从+1开始,避免删除当天数据 toArray(String[]::new); if (ArrayUtils.isNotEmpty(removeCols)) { return redisTemplate.opsForHash().delete(hashName, removeCols); } } catch (Exception ex) { ex.printStackTrace(); } return 0L; } 按照日期来生成分布式id,达到id不重复的目的,这也就是分布式id(不重复),看起来简单其实如果在高流量冲击下,需要考虑的东西要很多,比如:什么时候生成初始Id、在多个服务器保证服务器时间尽可能一样情况下,该保留多少日期hkey等; 就上面代码对初始Id就做的不是很好,在业务获取Id时候,会去检测并创建id,这样与redis交互就多了一次,通常可以用服务来一次性生成当前日期往后推n天的hkey,这样就避免了在业务获取id时候,还要去putIfAbsent一次验证,减少了请求次数。实在不行可以使用lua脚本放在一次请求去做put和increment,你可能会用到: RedisScript script = new DefaultRedisScript(""); redisTemplate.execute(script, Arrays.asList(""));
本篇和大家分享的是关于rabbit的生产和消费方的一些实用的操作;正如文章标题,主要内容如producer的confirm和consumer的ack,这两者使用的模式都是用来保证数据完整性,防止数据丢失。 producer的confirm模式 consumer的ack模式 producer的confirm模式 首先,有这样一种业务场景1:a系统在做活动前,需要给用户的手机发送一条活动内容短信希望用户来参加,因为用户量有点大,所以通过往短信mq中插入数据方式,让短信服务来消费mq发短信;此时插入mq消息的服务为了保证给所有用户发消息,并且要在短时间内插入完成(因此用到了异步插入方式(快速)),我们就需要知道每次插入mq是否成功,如果不成功那我们可以收集失败的信息后补发(因此confirm模式排上了用场);如图设计:在springboot中可以使用基于amqp封装的工厂类来开启confirm模式,然后通过RabbitTemplate模板来设置回调函数,如下代码: ///region producer生产 - confirm模式 public RabbitTemplate getRabbitTemplate(RabbitTemplate.ConfirmCallback confirmCallback) { return this.getRabbitTemplate(this.connectionFactory(), confirmCallback); } public RabbitTemplate getRabbitTemplate(CachingConnectionFactory connectionFactory, RabbitTemplate.ConfirmCallback confirmCallback) { RabbitTemplate template = new RabbitTemplate(connectionFactory); //product开启confirm模式 connectionFactory.setPublisherConfirms(true); //设置confirm回调处理 template.setConfirmCallback(confirmCallback); return template; } ///endregion 这里通过RabbitTemplate.ConfirmCallback函数编程来传递我们自定义的回调方法,如下收集confirm返回的结果信息: RabbitUtil rabbitUtil = new RabbitUtil(this.getFirstNode().getLink()); RabbitTemplate template = rabbitUtil.getRabbitTemplate((a, b, c) -> { System.out.println("firstNodeTpl - ConfirmCallback的Id:" + a.getId() + ";状态:" + b + ";信息:" + c); }); 最后再通过RabbitTemplate实例的convertAndSend方法发送mq信息,我们能够在日志中看到如下记录的信息:这里的状态true:表示send成功,false:表示send失败;通常false的时候信息c会有响应的错误提示,这里把网络断开,如下错误提示: consumer的ack模式 再来,有这样一种场景2:短信服务去消费mq队列信息时,倘若服务调用的运营商发送短信接口异常了(短信运营商接口欠费),我们此时的短信是发送失败的,用户也收不到短信,但是在默认(默认开启ack)前提下mq消息已经被消费了rabbit中没有记录了(kafka例外);想要mq消息在业务逻辑异常时还存在,那么可以使用ack方式; 在springboot中可以使用基于amqp封装的工厂类关闭自动ack模式,改为手动ack方式;只有当业务代码流程走完后,最后通过代码设置ack标识,来通知rabbit消息可以丢弃了;如果设置了手动模式后,又没有提交ack标识,那么mq中的消息一直存在无法释放(每次consumer消费后,rabbit会把noack的消息重复放入队列中): ///region consumer监听 - 手动ack public SimpleRabbitListenerContainerFactory listenerContainerFactory() { return this.listenerContainerFactory(this.connectionFactory()); } public SimpleRabbitListenerContainerFactory listenerContainerFactory(ConnectionFactory connectionFactory) { SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); factory.setConnectionFactory(connectionFactory); //代码手动ack factory.setAcknowledgeMode(AcknowledgeMode.MANUAL); //开启消费者数量 factory.setConcurrentConsumers(2); //每次接受数据量,默认250 factory.setPrefetchCount(300); return factory; } ///endregion 通过连接工厂设置手动ack方式,然后获取mq消息后,走完正常业务逻辑,最后再手动通知ack释放消息,如下: @RabbitListener(containerFactory = "firstNodeListener", queues = {"${shenniu.rabbits.firstNode.queue}"}) private void firstNodeListener(String msg, Channel channel, Message message) { try { long deliverTag = message.getMessageProperties().getDeliveryTag(); System.out.println("firstNodeListener - 消费消息 [" + deliverTag + "] - " + msg); channel.basicAck(deliverTag, true); } catch (Exception ex) { } } 这里ack主要根据mq消息的唯一编号(deliverTag)来通知;如果我们不设置ack确认,那么消息状态会是这样如下rabbit管理后台:
本章内容主要分享多个module中的实体类集合生成到一个jar包中,并且发布到远程库;这里采用maven-assembly-plugin插件的功能来操作打包,内容不长却贴近实战切值得拥有,主要节点内容如: 多个module实体类集合打jar包 jar包打入本地库 jar包上传至远程库 多个module实体类集合打jar包 首先假设下我们拥有多个module,每个module中有一些接口或公用方法的参数实体和响应实体,再或者有公用枚举,此时为了合作方对接方便,我们需要吧这些个实体类、枚举、接口等集合到一个jar包中,提供给合作方使用; 这里我们可以在项目工程下再创建一个空的module,里面不用包含任何代码,但是需要在pom中加入assmebly插件如: <plugins> <plugin> <artifactId>maven-assembly-plugin</artifactId> <version>3.1.0</version> <executions> <execution> <id>${project.artifactId}</id> <phase>package</phase> <goals> <goal>single</goal> </goals> <configuration> <appendAssemblyId>false</appendAssemblyId> <descriptors> <descriptor>${project.basedir}/src/main/assembly/assembly.xml</descriptor> </descriptors> </configuration> </execution> </executions> </plugin> </plugins> assmebly插件对应一个assembly.xml配置文件,该配置文件主要包含要打入的实体类,接口等的class类路径;没错我们主要通过吧class类文件加入到jar中,来完成我们开放的实体类等信息,assembly.xml文件信息如: <assembly xmlns="http://maven.apache.org/ASSEMBLY/2.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.0.0 http://maven.apache.org/xsd/assembly-2.0.0.xsd http://maven.apache.org/ASSEMBLY/2.0.0 "> <id>RELEASE</id> <formats> <format>jar</format> </formats> <includeBaseDirectory>false</includeBaseDirectory> <fileSets> <!-- 打包项目本身编译class文件 --> <fileSet> <directory>${project.build.directory}/classes</directory> <outputDirectory></outputDirectory> </fileSet> <!-- api包class文件 --> <fileSet> <directory>${project.parent.basedir}/api/target/classes</directory> <includes> <include>com/kuku/api/core/provider/**</include> </includes> <outputDirectory></outputDirectory> </fileSet> <!-- domain包class文件 --> <fileSet> <directory>${project.parent.basedir}/domain/target/classes</directory> <includes> <include>com/kuku/domain/master/**</include> <include>com/kuku/domain/work/dao/**</include> <include>com/kuku/domain/work/model/**</include> <include>entity/**</include> </includes> <outputDirectory></outputDirectory> </fileSet> <!-- common包class文件 --> <fileSet> <directory>${project.parent.basedir}/common/target/classes</directory> <outputDirectory></outputDirectory> </fileSet> <!-- 打包resources下的配置文件 --> <fileSet> <directory>${project.basedir}/src/main/resources</directory> <outputDirectory>conf</outputDirectory> <filtered>true</filtered> <includes> <include>static/**</include> <include>templates/**</include> <include>**/*</include> </includes> </fileSet> </fileSets> <!--<dependencySets>--> <!--<dependencySet>--> <!--<useProjectArtifact>false</useProjectArtifact>--> <!--<outputDirectory>lib</outputDirectory>--> <!--<scope>runtime</scope>--> <!--</dependencySet>--> <!--</dependencySets>--> </assembly> 来到此处我们配置就完成了,上面主要目的是去target/classes目录下查找生成的.class文件打入jar包中,具体效果就不展示了各自尝试吧。 jar包打入本地库 如果没有远程库或权限,可以考虑把jar包install到本地工程库,通常情况下通过mvn install即可完成;本地库路径一般在安装mvn指定,成功install后可以看到这几种文件: jar包上传至远程库 通常公司内部或组内协助完成项目,都会吧接口实体、枚举、公用的方法等打包放入公司内网的库中;假如我们有上传包到内网库的权限,可以首先配置mvn的settings.xml的servers节点: <servers> <server> <id>release</id> <username>shenniu003</username> <password>shenniu003</password> </server> </servers> 然后在项目pom中增加如下配置: <distributionManagement> <repository> <id>release</id> <url>http://102.0.102.10/nexus/content/repositories/releases/</url> <name>provider</name> </repository> </distributionManagement> 再然后执行mvn deploy命令(idea可以鼠标右键deploy选择run),该命令会打项目的jar并上传到指定的url内网库地址上
2019年09月
2019年08月
2019年07月