一、相关依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-mongodb</artifactId> <version>2.0.1.RELEASE</version> </dependency>
二、配置文件
spring.data.mongodb.uri=mongodb://adminUser:adminPass@localhost:27017/?authSource=admin&authMechanism=SCRAM-SHA-1 spring.data.mongodb.database=users
多个 IP 集群可以采用以下配置: spring.data.mongodb.uri=mongodb://user:pwd@ip1:port1,ip2:port2/databases
三、基本操作方法封装
package com.wcf.mongo.service; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import com.mongodb.client.ListIndexesIterable; import com.mongodb.client.model.IndexOptions; import com.mongodb.client.model.Indexes; import com.wcf.mongo.entity.MongoBaseInfo; import org.bson.Document; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.Update; import org.springframework.stereotype.Service; import org.springframework.util.ObjectUtils; import java.util.ArrayList; import java.util.List; import java.util.Map; /** * @author wangcanfeng * @description 简单的mongodb使用接口 * @Date Created in 17:24-2019/3/20 */ @Service public class SimpleMongoServiceImpl<T extends MongoBaseInfo> implements SimpleMongoService<T> { /** * 注入template,减少重复代码 */ @Autowired private MongoTemplate mongoTemplate; /** * 功能描述: 创建一个集合 * 同一个集合中可以存入多个不同类型的对象,我们为了方便维护和提升性能, * 后续将限制一个集合中存入的对象类型,即一个集合只能存放一个类型的数据 * * @param name 集合名称,相当于传统数据库的表名 * @return:void * @since: v1.0 * @Author:wangcanfeng * @Date: 2019/3/20 17:27 */ @Override public void createCollection(String name) { mongoTemplate.createCollection(name); } /** * 功能描述: 创建索引 * 索引是顺序排列,且唯一的索引 * * @param collectionName 集合名称,相当于关系型数据库中的表名 * @param filedName 对象中的某个属性名 * @return:java.lang.String * @since: v1.0 * @Author:wangcanfeng * @Date: 2019/3/20 16:13 */ @Override public String createIndex(String collectionName, String filedName) { //配置索引选项 IndexOptions options = new IndexOptions(); // 设置为唯一 options.unique(true); //创建按filedName升序排的索引 return mongoTemplate.getCollection(collectionName).createIndex(Indexes.ascending(filedName), options); } /** * 功能描述: 获取当前集合对应的所有索引的名称 * * @param collectionName * @return:java.util.List<java.lang.String> * @since: v1.0 * @Author:wangcanfeng * @Date: 2019/3/20 16:46 */ @Override public List<String> getAllIndexes(String collectionName) { ListIndexesIterable<Document> list = mongoTemplate.getCollection(collectionName).listIndexes(); //上面的list不能直接获取size,因此初始化arrayList就不设置初始化大小了 List<String> indexes = new ArrayList<>(); for (Document document : list) { document.entrySet().forEach((key) -> { //提取出索引的名称 if (key.getKey().equals("name")) { indexes.add(key.getValue().toString()); } }); } return indexes; } /** * 功能描述: 往对应的集合中插入一条数据 * * @param info 存储对象 * @param collectionName 集合名称 * @return:void * @since: v1.0 * @Author:wangcanfeng * @Date: 2019/3/20 16:46 */ @Override public void insert(T info, String collectionName) { mongoTemplate.insert(info, collectionName); } /** * 功能描述: 往对应的集合中批量插入数据,注意批量的数据中不要包含重复的id * * @param infos 对象列表 * @return:void * @since: v1.0 * @Author:wangcanfeng * @Date: 2019/3/20 16:47 */ @Override public void insertMulti(List<T> infos, String collectionName) { mongoTemplate.insert(infos, collectionName); } /** * 功能描述: 使用索引信息精确更改某条数据 * * @param id 唯一键 * @param collectionName 集合名称 * @param info 待更新的内容 * @return:void * @since: v1.0 * @Author:wangcanfeng * @Date: 2019/3/20 18:42 */ @Override public void updateById(String id, String collectionName, T info) { Query query = new Query(Criteria.where("id").is(id)); Update update = new Update(); String str = JSON.toJSONString(info); JSONObject jQuery = JSON.parseObject(str); jQuery.forEach((key, value) -> { //因为id相当于传统数据库中的主键,这里使用时就不支持更新,所以需要剔除掉 if (!key.equals("id")) { update.set(key, value); } }); mongoTemplate.updateMulti(query, update, info.getClass(), collectionName); } /** * 功能描述: 根据id删除集合中的内容 * * @param id 序列id * @param collectionName 集合名称 * @param clazz 集合中对象的类型 * @return:void * @since: v1.0 * @Author:wangcanfeng * @Date: 2019/3/20 16:47 */ @Override public void deleteById(String id, Class<T> clazz, String collectionName) { // 设置查询条件,当id=#{id} Query query = new Query(Criteria.where("id").is(id)); // mongodb在删除对象的时候会判断对象类型,如果你不传入对象类型,只传入了集合名称,它是找不到的 // 上面我们为了方便管理和提升后续处理的性能,将一个集合限制了一个对象类型,所以需要自行管理一下对象类型 // 在接口传入时需要同时传入对象类型 mongoTemplate.remove(query, clazz, collectionName); } /** * 功能描述: 根据id查询信息 * * @param id 注解 * @param clazz 类型 * @param collectionName 集合名称 * @return:java.util.List<T> * @since: v1.0 * @Author:wangcanfeng * @Date: 2019/3/20 16:47 */ @Override public T selectById(String id, Class<T> clazz, String collectionName) { // 查询对象的时候,不仅需要传入id这个唯一键,还需要传入对象的类型,以及集合的名称 return mongoTemplate.findById(id, clazz, collectionName); } /** * 功能描述: 查询列表信息 * 将集合中符合对象类型的数据全部查询出来 * * @param collectName 集合名称 * @param clazz 类型 * @return:java.util.List<T> * @since: v1.0 * @Author:wangcanfeng * @Date: 2019/3/21 10:38 */ @Override public List<T> selectList(String collectName, Class<T> clazz) { return selectList(collectName, clazz, null, null); } /** * 功能描述: 分页查询列表信息 * * @param collectName 集合名称 * @param clazz 对象类型 * @param currentPage 当前页码 * @param pageSize 分页大小 * @return:java.util.List<T> * @since: v1.0 * @Author:wangcanfeng * @Date: 2019/3/21 10:38 */ @Override public List<T> selectList(String collectName, Class<T> clazz, Integer currentPage, Integer pageSize) { //设置分页参数 Query query = new Query(); //设置分页信息 if (!ObjectUtils.isEmpty(currentPage) && ObjectUtils.isEmpty(pageSize)) { query.limit(pageSize); query.skip(pageSize * (currentPage - 1)); } return mongoTemplate.find(query, clazz, collectName); } /** * 功能描述: 根据条件查询集合 * * @param collectName 集合名称 * @param conditions 查询条件,目前查询条件处理的比较简单,仅仅做了相等匹配,没有做模糊查询等复杂匹配 * @param clazz 对象类型 * @param currentPage 当前页码 * @param pageSize 分页大小 * @return:java.util.List<T> * @since: v1.0 * @Author:wangcanfeng * @Date: 2019/3/21 10:48 */ @Override public List<T> selectByCondition(String collectName, Map<String, String> conditions, Class<T> clazz, Integer currentPage, Integer pageSize) { if (ObjectUtils.isEmpty(conditions)) { return selectList(collectName, clazz, currentPage, pageSize); } else { //设置分页参数 Query query = new Query(); query.limit(pageSize); query.skip(currentPage); // 往query中注入查询条件 conditions.forEach((key, value) -> query.addCriteria(Criteria.where(key).is(value))); return mongoTemplate.find(query, clazz, collectName); } } }
四、连接池配置
mongodb: address: localhost:27017 database: soms username: admin password: 123456 # 连接池配置 clientName: soms-task # 客户端的标识,用于定位请求来源等 connectionTimeoutMs: 10000 # TCP连接超时,毫秒 readTimeoutMs: 15000 # TCP读取超时,毫秒 poolMaxWaitTimeMs: 3000 #当连接池无可用连接时客户端阻塞等待的时长,单位毫秒 connectionMaxIdleTimeMs: 60000 #TCP连接闲置时间,单位毫秒 connectionMaxLifeTimeMs: 120000 #TCP连接最多可以使用多久,单位毫秒 heartbeatFrequencyMs: 20000 #心跳检测发送频率,单位毫秒 minHeartbeatFrequencyMs: 8000 #最小的心跳检测发送频率,单位毫秒 heartbeatConnectionTimeoutMs: 10000 #心跳检测TCP连接超时,单位毫秒 heartbeatReadTimeoutMs: 15000 #心跳检测TCP连接读取超时,单位毫秒 connectionsPerHost: 100 # 每个host的TCP连接数 minConnectionsPerHost: 5 #每个host的最小TCP连接数 #计算允许多少个线程阻塞等待可用TCP连接时的乘数,算法: threadsAllowedToBlockForConnectionMultiplier*connectionsPerHost,当前配置允许10*20个线程阻塞 threadsAllowedToBlockForConnectionMultiplier: 10
配置类
import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; import org.springframework.validation.annotation.Validated; import javax.validation.constraints.Min; import javax.validation.constraints.NotNull; import javax.validation.constraints.Size; import java.util.List; @Data @Validated @Component @ConfigurationProperties(prefix = "mongodb") public class MongoClientOptionProperties { /** 基础连接参数 */ private String database; private String username; private String password; @NotNull private List<String> address; /** 客户端连接池参数 */ @NotNull @Size(min = 1) private String clientName; /** socket连接超时时间 */ @Min(value = 1) private int connectionTimeoutMs; /** socket读取超时时间 */ @Min(value = 1) private int readTimeoutMs; /** 连接池获取链接等待时间 */ @Min(value = 1) private int poolMaxWaitTimeMs; /** 连接闲置时间 */ @Min(value = 1) private int connectionMaxIdleTimeMs; /** 连接最多可以使用多久 */ @Min(value = 1) private int connectionMaxLifeTimeMs; /** 心跳检测发送频率 */ @Min(value = 2000) private int heartbeatFrequencyMs; /** 最小的心跳检测发送频率 */ @Min(value = 300) private int minHeartbeatFrequencyMs; /** 计算允许多少个线程阻塞等待时的乘数,算法:threadsAllowedToBlockForConnectionMultiplier*connectionsPerHost */ @Min(value = 1) private int threadsAllowedToBlockForConnectionMultiplier; /** 心跳检测连接超时时间 */ @Min(value = 200) private int heartbeatConnectionTimeoutMs; /** 心跳检测读取超时时间 */ @Min(value = 200) private int heartbeatReadTimeoutMs; /** 每个host最大连接数 */ @Min(value = 1) private int connectionsPerHost; /** 每个host的最小连接数 */ @Min(value = 1) private int minConnectionsPerHost;
连接池配置
import com.mongodb.MongoClient; import com.mongodb.MongoClientOptions; import com.mongodb.MongoCredential; import com.mongodb.ServerAddress; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.mongodb.MongoDbFactory; import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.data.mongodb.core.SimpleMongoDbFactory; import org.springframework.data.mongodb.core.convert.*; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; import java.util.ArrayList; import java.util.List; @Configuration public class MongoConfig { private final Logger log = LoggerFactory.getLogger(MongoConfig.class); /** * 自定义mongo连接池 * * @param properties * @return */ @Bean @Autowired public MongoDbFactory mongoDbFactory(MongoClientOptionProperties properties) { //创建客户端参数 MongoClientOptions options = mongoClientOptions(properties); //创建客户端和Factory List<ServerAddress> serverAddresses = new ArrayList<>(); for (String address : properties.getAddress()) { String[] hostAndPort = address.split(":"); String host = hostAndPort[0]; int port = Integer.parseInt(hostAndPort[1]); ServerAddress serverAddress = new ServerAddress(host, port); serverAddresses.add(serverAddress); } String username = properties.getUsername(); String password = properties.getPassword(); String database = properties.getDatabase(); MongoClient mongoClient; if (StringUtils.isNotEmpty(username) && StringUtils.isNotEmpty(password)) { //创建认证客户端 MongoCredential mongoCredential = MongoCredential.createScramSha1Credential( username, database, password.toCharArray()); mongoClient = new MongoClient(serverAddresses.get(0), mongoCredential, options); } else { //创建非认证客户端 mongoClient = new MongoClient(serverAddresses, options); } SimpleMongoDbFactory mongoDbFactory = new SimpleMongoDbFactory(mongoClient, database); log.info("mongodb注入成功"); return mongoDbFactory; } @Bean(name = "mongoTemplate") @Autowired public MongoTemplate getMongoTemplate(MongoDbFactory mongoDbFactory) { return new MongoTemplate(mongoDbFactory); } /** * mongo客户端参数配置 * * @return */ public MongoClientOptions mongoClientOptions(MongoClientOptionProperties properties) { return MongoClientOptions.builder() .connectTimeout(properties.getConnectionTimeoutMs()) .socketTimeout(properties.getReadTimeoutMs()).applicationName(properties.getClientName()) .heartbeatConnectTimeout(properties.getHeartbeatConnectionTimeoutMs()) .heartbeatSocketTimeout(properties.getHeartbeatReadTimeoutMs()) .heartbeatFrequency(properties.getHeartbeatFrequencyMs()) .minHeartbeatFrequency(properties.getMinHeartbeatFrequencyMs()) .maxConnectionIdleTime(properties.getConnectionMaxIdleTimeMs()) .maxConnectionLifeTime(properties.getConnectionMaxLifeTimeMs()) .maxWaitTime(properties.getPoolMaxWaitTimeMs()) .connectionsPerHost(properties.getConnectionsPerHost()) .threadsAllowedToBlockForConnectionMultiplier( properties.getThreadsAllowedToBlockForConnectionMultiplier()) .minConnectionsPerHost(properties.getMinConnectionsPerHost()).build(); } @Bean public MappingMongoConverter mappingMongoConverter(MongoDbFactory factory, MongoMappingContext context, BeanFactory beanFactory) { DbRefResolver dbRefResolver = new DefaultDbRefResolver(factory); MappingMongoConverter mappingConverter = new MappingMongoConverter(dbRefResolver, context); try { mappingConverter.setCustomConversions(beanFactory.getBean(MongoCustomConversions.class)); } catch (NoSuchBeanDefinitionException ignore) { } // Don"t save _class to dao mappingConverter.setTypeMapper(new DefaultMongoTypeMapper(null)); return mappingConverter; } }
五、问题集
1、mongodb 3.0认证问题
MongoDB’s implementation of SCRAM-SHA-1 represents an improvement in security over the previously-used MONGODB-CR, providing: * A tunable work factor (iterationCount), * Per-user random salts rather than server-wide salts, * A cryptographically stronger hash function (SHA-1 rather than MD5), and * Authentication of the server to the client as well as the client to the server.
MongoDB 3.0新增了一种认证机制(authenticationMechanisms) SCRAM-SHA-1, 并把他设置为默认的方式. 而Spring Boot里默认使用旧的认证机制. 这就造成了不一致从而认证通不过. 解决方法有两种:
(1) 把Mongodb的认证机制改了: mongodb支持如下几种:
SCRAM-SHA-1 MONGODB-CR MONGODB-X509 GSSAPI (Kerberos) PLAIN (LDAP SASL)
把Mongodb的认证方式改变一下自然能解决问题. 可以同时支持多个.
setParameter: authenticationMechanisms: MONGODB-CR,SCRAM-SHA-1 enableLocalhostAuthBypass: false logLevel: 4
但是既然MongoDB从3.0开始用SCRAM-SHA-1作为默认,应该是有道理的, 比如安全性方面比MONGODB-CR更好之类的.
(2)改变认证方式, 就只能改java代码了 我们看一下Spring Boot的源码; org.springframework.boot.autoconfigure.mongo.MongoProperties 的 createMongoClient方法:
public MongoClient createMongoClient(MongoClientOptions options) throws UnknownHostException { try { if (hasCustomAddress() || hasCustomCredentials()) { if (options == null) { options = MongoClientOptions.builder().build(); } List<MongoCredential> credentials = null; if (hasCustomCredentials()) { String database = this.authenticationDatabase == null ? getMongoClientDatabase() : this.authenticationDatabase; credentials = Arrays.asList(MongoCredential.createMongoCRCredential( this.username, database, this.password)); } String host = this.host == null ? "localhost" : this.host; int port = this.port == null ? DEFAULT_PORT : this.port; return new MongoClient(Arrays.asList(new ServerAddress(host, port)), credentials, options); } // The options and credentials are in the URI return new MongoClient(new MongoClientURI(this.uri, builder(options))); } finally { clearPassword(); } }
可以看到它是用MongoCredential.createMongoCRCredential方法来创建认证信息, 并且没有留出任何公开的接口让你改变这一行为. 这个真是不应该呀. 找到问题所在, 其实解决就非常方便了, 使用createScramSha1Credential方法既可.
首先创建一个MongoDBConfiguration类, 用于创建MongoClient实例.
@Configuration @EnableConfigurationProperties(MongoProperties.class) public class MongoDBConfiguration { @Autowired private MongoProperties properties; @Autowired(required = false) private MongoClientOptions options; private Mongo mongo; @PreDestroy public void close() { if (this.mongo != null) { this.mongo.close(); } } @Bean public Mongo mongo() throws UnknownHostException { this.mongo = this.properties.createMongoClient(this.options); return this.mongo; } }
2、解决SpringBoot MongoDB插入文档默认生成_class字段问题
@Configuration
public class SpringMongoConfig{
@Bean
public MongoTemplate mongoTemplate() throws Exception {
//remove _class
MappingMongoConverter converter =
new MappingMongoConverter(mongoDbFactory(), new MongoMappingContext());
converter.setTypeMapper(new DefaultMongoTypeMapper(null));
MongoTemplate mongoTemplate = new MongoTemplate(mongoDbFactory(), converter);
return mongoTemplate;
}
}
3、spring boot 集成mongodb 开启事务
@Configuration
public class TransactionConfig {
@Bean
MongoTransactionManager transactionManager(MongoDbFactory factory){
return new MongoTransactionManager(factory);
}
}