前提:在可运行的SpringBoot的项目内引用以下JAR包
整个工具的代码都在Gitee或者Github地址内
gitee:solomon-parent: 这个项目主要是总结了工作上遇到的问题以及学习一些框架用于整合例如:rabbitMq、reids、Mqtt、S3协议的文件服务器、mongodb
需要引入的JAR包(版本根据自身要求使用,本教程用的版本均为最新)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
1.写一个mongodb注解
@Persistent
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE })
@Component
public @interface MongoDBCapped {
@AliasFor(annotation = Component.class)
String value() default "";
/**
* 限制记录大小使用的是(字节)
* @return
*/
long size() default 1024;
/**
* 限制记录行数
* @return
*/
long maxDocuments() default 0;
}
2.编写一个AOP切面代码,切换租户
/**
* mongodb 租户切换的AOP实现类
*/
@Aspect
@Configuration
public class MongoAspect {
private final Logger logger = LoggerUtils.logger(getClass());
@Resource
private MongoTenantsContext context;
@Value("${spring.data.mongodb.mode}")
private String mode;
@Pointcut("execution(* org.springframework.data.mongodb.core.MongoTemplate.*(..)) ||"
+ "execution(* org.springframework.data.mongodb.core.MongoOperations.*(..)) ")
void cutPoint() {}
@Around("cutPoint()")
public Object around(ProceedingJoinPoint point) throws Throwable {
boolean isSwitch = ValidateUtils.equals(mode, SwitchModeEnum.SWITCH_DB.toString());
try {
String tenantCode = isSwitch ? RequestHeaderHolder.getTenantCode() : BaseCode.DEFAULT;
logger.info("mongo切换数据源,租户编码为:{}", tenantCode);
context.setFactory(tenantCode);
return point.proceed();
} finally {
context.removeFactory();
}
}
}
3.编写mongodb配置
@Configuration
@EnableConfigurationProperties(value={MongoProperties.class,TenantMongoProperties.class})
@Import(value = {MongoTenantsContext.class})
@Order(2)
public class MongoConfig {
private Logger logger = LoggerUtils.logger(getClass());
private final TenantMongoProperties mongoProperties;
private final MongoTenantsContext context;
private final MongoProperties properties;
private boolean isSwitchDb = false;
private final ApplicationContext applicationContext;
public MongoConfig(TenantMongoProperties mongoProperties, MongoTenantsContext context,
MongoProperties properties, ApplicationContext applicationContext) {
this.mongoProperties = mongoProperties;
this.context = context;
this.properties = properties;
this.isSwitchDb = ValidateUtils.equalsIgnoreCase(SwitchModeEnum.SWITCH_DB.toString(), mongoProperties.getMode().toString());
this.applicationContext = applicationContext;
}
@PostConstruct
public void afterPropertiesSet() {
logger.info("mongoDb当前模式为:{}",mongoProperties.getMode().getDesc());
SpringUtil.setContext(applicationContext);
if (isSwitchDb) {
Map<String, MongoProperties> tenantMap = ValidateUtils.isEmpty(mongoProperties.getTenant()) ? new HashMap<>() : mongoProperties.getTenant();
if(!tenantMap.containsKey(BaseCode.DEFAULT)){
tenantMap.put(BaseCode.DEFAULT, properties);
mongoProperties.setTenant(tenantMap);
}
MongoDbUtils.init(mongoProperties.getTenant(), context);
} else {
SimpleMongoClientDatabaseFactory factory = MongoDbUtils.initFactory(properties);
MongoDbUtils.initDocument(factory);
context.setFactory(BaseCode.DEFAULT,factory);
}
}
@Bean(name = "mongoTemplate")
@ConditionalOnMissingBean(MongoTemplate.class)
public MongoTemplate dynamicMongoTemplate() {
SimpleMongoClientDatabaseFactory factory = context.getFactoryMap().values().iterator().next();
DbRefResolver dbRefResolver = new DefaultDbRefResolver(factory);
MappingMongoConverter mappingConverter = new MappingMongoConverter(dbRefResolver, new MongoMappingContext());
List<Object> list = new ArrayList<>();
list.add(new LocalDateTimeToDateConverter());
list.add(new DateToLocalDateTimeConverter());
mappingConverter.setCustomConversions(new MongoCustomConversions(list));
mappingConverter.setTypeMapper(new DefaultMongoTypeMapper(null));
return isSwitchDb ? new DynamicMongoTemplate(factory, mappingConverter) : new MongoTemplate(factory, mappingConverter);
}
@Bean(name = "mongoDbFactory")
@ConditionalOnMissingBean(MongoDatabaseFactory.class)
public MongoDatabaseFactory tenantMongoDbFactory() {
return context.getFactoryMap().values().iterator().next();
}
}
@Configuration
public class MongoTenantsContext extends TenantContext<SimpleMongoClientDatabaseFactory> {
@Override
public SimpleMongoClientDatabaseFactory getFactory() {
return THREAD_LOCAL.get();
}
@Override
public void setFactory(String key) {
THREAD_LOCAL.set(FACTORY_MAP.get(key));
}
@Override
public void removeFactory() {
THREAD_LOCAL.remove();
}
@Override
public Map<String, SimpleMongoClientDatabaseFactory> getFactoryMap() {
return FACTORY_MAP;
}
@Override
public void setFactory(Map<String, SimpleMongoClientDatabaseFactory> factoryMap) {
FACTORY_MAP.putAll(factoryMap);
}
@Override
public void setFactory(String key, SimpleMongoClientDatabaseFactory factory) {
FACTORY_MAP.put(key,factory);
}
}
public abstract class TenantContext<F> {
protected ThreadLocal<F> THREAD_LOCAL = new ThreadLocal<>();
protected Map<String, F> FACTORY_MAP = new ConcurrentHashMap<>();
/**
* 获取工厂
* @return
*/
public abstract F getFactory();
/**
* 设置工厂
* @param key
*/
public abstract void setFactory(String key);
/**
* 删除工厂
*/
public abstract void removeFactory();
/**
* 获取所有工厂
* @return
*/
public abstract Map<String,F> getFactoryMap();
/**
* set工厂
* @param factoryMap
*/
public abstract void setFactory(Map<String, F> factoryMap);
/**
* set工厂
* @param key
* @param factory
*/
public abstract void setFactory(String key,F factory);
}
4.mongodb转换器
public class DateToLocalDateTimeConverter implements Converter<Date, LocalDateTime> {
@Override
public LocalDateTime convert(Date date) {
return date.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
}
}
public class LocalDateTimeToDateConverter implements Converter<LocalDateTime, Date> {
@Override
public Date convert(LocalDateTime dateTime) {
return Date.from(dateTime.atZone( ZoneId.systemDefault()).toInstant());
}
}
5.租户mongo配置
@ConfigurationProperties(prefix = "spring.data.mongodb")
public class TenantMongoProperties {
public Map<String,MongoProperties> tenant;
/**
* mongo缓存模式(默认单库)
*/
private SwitchModeEnum mode = SwitchModeEnum.NORMAL;
public SwitchModeEnum getMode() {
return mode;
}
public void setMode(SwitchModeEnum mode) {
this.mode = mode;
}
public Map<String, MongoProperties> getTenant() {
return tenant;
}
public void setTenant(Map<String, MongoProperties> tenant) {
this.tenant = tenant;
}
}
6.写一个MongoRepository基类(T:实体类,I:id类型)
public class MongoRepository<T, I> {
protected Class<T> modelClass = (Class) ((ParameterizedType) this.getClass().getGenericSuperclass()).getActualTypeArguments()[0];
protected static String ID = "_id";
private final MongoTemplate mongoTemplate;
public MongoRepository(MongoTemplate mongoTemplate) {this.mongoTemplate = mongoTemplate;}
/**
* 获取一条记录
*/
public T get(Query query) {
return mongoTemplate.findOne(query, modelClass);
}
/**
* 获取一条记录
*/
public <V> V get(Query query, Class<V> clazz) {
return mongoTemplate.findOne(query, clazz);
}
/**
* 根据id获取记录
*/
public T getById(I id) {
Query query = new Query();
query.addCriteria(Criteria.where(ID).is(id));
return mongoTemplate.findOne(query, modelClass);
}
/**
* 根据id获取记录
*/
public <V> V getById(I id, Class<V> clazz) {
Query query = new Query();
query.addCriteria(Criteria.where(ID).is(id));
return mongoTemplate.findOne(query, clazz);
}
/**
* 查询列表
*/
public List<T> find(Query query) {
return mongoTemplate.find(query, modelClass);
}
/**
* 查询列表
*/
public <V> List<V> find(Query query, Class<V> clazz) {
return mongoTemplate.find(query, clazz);
}
/**
* 保存(非覆盖保存)
*/
public T save(T entity) {
return mongoTemplate.save(entity);
}
/**
* 保存(非覆盖保存)
*/
public Collection<T> save(Collection<T> entity) {
entity.forEach(data -> {
this.save(data);
});
return entity;
}
/**
* 保存(覆盖保存)
*/
public T insert(T entity) {
return mongoTemplate.insert(entity);
}
/**
* 保存(覆盖保存)
*/
public Collection<T> insert(Collection<T> entity) {
return mongoTemplate.insertAll(entity);
}
/**
* 统计记录数
*
* @param query
* @return
*/
public long count(Query query) {
return mongoTemplate.count(query, modelClass);
}
/**
* 根据id删除记录
*/
public void deleteById(I id) {
Query query = new Query(Criteria.where("_id").is(id));
mongoTemplate.remove(query, modelClass);
}
/**
* 删除记录
*/
public void delete(Query query) {
mongoTemplate.remove(query, modelClass);
}
/**
* 更新记录
*
* @param query
* @param update
*/
public void update(Query query, Update update) {
mongoTemplate.updateFirst(query, update, modelClass);
}
/**
* 判断记录是否存在
*/
public boolean exists(Query query) {
long count = count(query);
return count > 0 ? true : false;
}
/**
* 分组查询
*/
public List<T> aggregate(List<AggregationOperation> list) {
return aggregate(list,modelClass);
}
/**
* 分组查询
*/
public List<T> aggregate(List<AggregationOperation> list,Class<T> clazz) {
AggregationResults<T> results = mongoTemplate.aggregate(Aggregation.newAggregation(list), mongoTemplate.getCollectionName(modelClass), clazz);
return results.getMappedResults();
}
/**
* 普通分页查询
*/
public PageVO<T> page(Query query, BasePageParam basePageParam) {
return page(query,basePageParam,modelClass);
}
/**
* 普通分页查询
*/
public PageVO<T> page(Query query, BasePageParam basePageParam,Class<T> clazz) {
//统计记录数
long total = count(query);
PageVO page = new PageVO<>(null, total, basePageParam.getPageNo(), basePageParam.getPageSize());
if (total <= 0) {
return page;
}
Pageable pageable = PageRequest.of((basePageParam.getPageNo() - 1) * basePageParam.getPageSize(), basePageParam.getPageSize(),sort(basePageParam));
query.with(pageable);
page.setData(this.find(query,clazz));
return page;
}
public PageVO<T> aggregate(List<AggregationOperation> list, Criteria criteria,BasePageParam basePageParam) {
return aggregate(list,criteria,basePageParam,modelClass);
}
public PageVO<T> aggregate(List<AggregationOperation> list, Criteria criteria,BasePageParam basePageParam,Class<T> clazz) {
long total = count(ValidateUtils.isNotEmpty(criteria) ? new Query(criteria) : new Query());
PageVO page = new PageVO<>(null, total, basePageParam.getPageNo(), basePageParam.getPageSize());
if (total <= 0) {
return page;
}
list.addAll(page(basePageParam));
AggregationResults<T> results = mongoTemplate.aggregate(Aggregation.newAggregation(list), mongoTemplate.getCollectionName(modelClass), clazz);
page.setData(results.getMappedResults());
return page;
}
public Sort sort(BasePageParam basePageParam) {
List<BasePageParam.Sort> sorted = basePageParam.getSorted();
if (ValidateUtils.isNotEmpty(sorted)) {
return Sort.by(getSort(basePageParam));
} else {
return Sort.unsorted();
}
}
//获取多个排序方式
public List<Sort.Order> getSort(BasePageParam basePageParam) {
List<BasePageParam.Sort> sorted = basePageParam.getSorted();
List<Sort.Order> querySortList = new ArrayList<>();
if (ValidateUtils.isNotEmpty(sorted)) {
for (BasePageParam.Sort sort : sorted) {
String orderByField = sort.getOrderByField();
OrderByEnum orderBy = sort.getOrderByMethod();
if (orderBy.name().equalsIgnoreCase(OrderByEnum.DESCEND.name())) {
querySortList.add(Sort.Order.desc(orderByField));
} else {
querySortList.add(Sort.Order.asc(orderByField));
}
}
}
return querySortList;
}
public List<AggregationOperation> page(BasePageParam param){
List<AggregationOperation> aggregationList = new ArrayList<>();
int skip = (param.getPageNo() - 1) * param.getPageSize();
aggregationList.add(Aggregation.skip(Long.valueOf(skip)));
aggregationList.add(Aggregation.limit(param.getPageSize()));
return aggregationList;
}
}
7.写一个租户的template
public class DynamicMongoTemplate extends MongoTemplate {
public DynamicMongoTemplate(MongoDatabaseFactory mongoDbFactory) {
super(mongoDbFactory);
}
public DynamicMongoTemplate(MongoDatabaseFactory mongoDbFactory, @Nullable MongoConverter mongoConverter) {
super(mongoDbFactory,mongoConverter);
}
@Override
protected MongoDatabase doGetDatabase() {
MongoDatabaseFactory mongoDbFactory = SpringUtil.getBean(MongoTenantsContext.class).getFactory();
return mongoDbFactory == null ? super.doGetDatabase() : mongoDbFactory.getMongoDatabase();
}
@Override
public MongoDatabaseFactory getMongoDbFactory() {
MongoDatabaseFactory mongoDbFactory = SpringUtil.getBean(MongoTenantsContext.class).getFactory();
return mongoDbFactory == null ? super.getMongoDbFactory() : mongoDbFactory;
}
@Override
public MongoDatabaseFactory getMongoDatabaseFactory() {
MongoDatabaseFactory mongoDbFactory = SpringUtil.getBean(MongoTenantsContext.class).getFactory();
return mongoDbFactory == null ? super.getMongoDatabaseFactory() : mongoDbFactory;
}
@Override
protected MongoCollection<Document> doCreateCollection(String collectionName, Document collectionOptions) {
return super.doCreateCollection(collectionName,collectionOptions);
}
}
8.编写一个mongodb工具类
public class MongoDbUtils {
private static final Logger logger = LoggerUtils.logger(MongoDbUtils.class);
public static void init(Map<String, MongoProperties> propertiesMap, MongoTenantsContext context){
for(Entry<String,MongoProperties> entry : propertiesMap.entrySet()){
init(entry.getKey(),entry.getValue(),context);
}
}
public static void init(String tenantCode,MongoProperties properties,MongoTenantsContext context){
SimpleMongoClientDatabaseFactory factory = initFactory(properties);
context.setFactory(tenantCode,factory);
List<String> collectionNameList = new ArrayList<>();
MongoDatabase mongoDatabase = factory.getMongoDatabase();
mongoDatabase.listCollectionNames().forEach(name->{
collectionNameList.add(name);
});
initDocument(factory);
}
public static SimpleMongoClientDatabaseFactory initFactory(MongoProperties properties){
MongoCredential mongoCredential = MongoCredential.createCredential(properties.getUsername(),properties.getDatabase(),properties.getPassword());
MongoClientSettings settings = MongoClientSettings.builder().credential(mongoCredential).applyToClusterSettings(builder -> {
builder.hosts(Arrays.asList(new ServerAddress(properties.getHost(),properties.getPort()))).mode(
ClusterConnectionMode.MULTIPLE).requiredClusterType(ClusterType.STANDALONE);
}).build();
return new SimpleMongoClientDatabaseFactory(MongoClients.create(settings),properties.getDatabase());
}
public static void initDocument(MongoDatabaseFactory factory){
List<String> collectionNameList = new ArrayList<>();
MongoDatabase mongoDatabase = factory.getMongoDatabase();
mongoDatabase.listCollectionNames().forEach(name->{
collectionNameList.add(name);
});
for(Object obj : SpringUtil.getBeansWithAnnotation(MongoDBCapped.class).values()){
MongoDBCapped mongoDBCapped = AnnotationUtils
.getAnnotation(obj.getClass(), MongoDBCapped.class);
org.springframework.data.mongodb.core.mapping.Document document = AnnotationUtils.getAnnotation(obj.getClass(), org.springframework.data.mongodb.core.mapping.Document.class);
String name = ValidateUtils.isNotEmpty(document.collection()) ? document.collection() : ValidateUtils.isNotEmpty(document.value()) ? document.value() : "";
boolean isCreate = collectionNameList.contains(name);
if(!isCreate){
if(ValidateUtils.isNotEmpty(mongoDBCapped)){
mongoDatabase.createCollection(name,new CreateCollectionOptions().capped(true).maxDocuments(mongoDBCapped.maxDocuments()).sizeInBytes(mongoDBCapped.size()));
} else {
mongoDatabase.createCollection(name);
}
} else {
logger.error("集合{}已存在,不允许修改为固定集合,防止数据丢失,请先备份数据后,手动创建固定集合",name);
// if(ValidateUtils.isNotEmpty(mongoDBCapped)){
// org.bson.Document command = new org.bson.Document("collStats", name);
// Boolean isCapped = mongoDatabase.runCommand(command).getBoolean("capped");
// if(!isCapped){
// logger.info("修改集合{}为固定集合,限制字节为:{},记录数:{}",name,mongoDBCapped.size(),mongoDBCapped.maxDocuments());
// command = new org.bson.Document("convertToCapped", name).append("capped",true).append("size", mongoDBCapped.size()).append("max",mongoDBCapped.maxDocuments());
// mongoDatabase.runCommand(command);
// }
// }
}
}
}
/**
* 检测数据库是否是固定集合
* @param collectionName 集合名称
* @return
*/
public static Document cherCheckCapped(String collectionName){
return new Document("collStats", collectionName);
}
public static void checkCapped(MongoDatabase database, String collectionName, int size, int maxDocuments) {
List<String> a= new ArrayList<>();
database.listCollectionNames().forEach(s -> {
a.add(s);
});
if (a.contains(collectionName)) {
Document command = new Document("collStats", collectionName);
Boolean isCapped = database.runCommand(command, ReadPreference.primary()).getBoolean("capped");
if (!isCapped) {
command = new Document("convertToCapped", collectionName).append("size", size).append("max",maxDocuments).append("capped",true);
database.runCommand(command, ReadPreference.primary());
}
} else {
database.createCollection(collectionName,new CreateCollectionOptions().capped(true).maxDocuments(maxDocuments).sizeInBytes(size));
}
}
/**
* 转换固定集合语句
* @param collectionName 集合名称
* @param max max则表示集合中文档的最大数量
* @param size 集合的大小,单位为kb
*/
public static Document convertToCapped(String collectionName,Long max,Long size){
if(ValidateUtils.isEmpty(collectionName)){
return null;
}
Document doc = new Document();
doc.put("convertToCapped",collectionName);
doc.put("capped",true);
if(ValidateUtils.isNotEmpty(size)){
doc.put("maxSize",size);
}
if (ValidateUtils.isNotEmpty(max)) {
doc.put("max",max);
}
return doc;
}
/**
* 创建mongodb数据库以及集合(创建数据库不创建集合会导致mongdb数据库自动删除)
* @param mongoClient mongodb连接
* @param dbName 数据库名称
* @param collectionName 集合名词
* @return
*/
public static MongoDatabase createDb(String mongoClient,String dbName,String collectionName){
if(ValidateUtils.isEmpty(mongoClient) || ValidateUtils.isEmpty(dbName) || ValidateUtils.isEmpty(collectionName)){
return null;
}
MongoDatabase mongoDatabase = createDb(mongoClient,dbName);
createCollection(mongoDatabase, dbName);
return mongoDatabase;
}
/**
* 创建用户并赋予权限
* @param mongoClient mongodb连接
* @param userName 用户名
* @param password 密码
* @param roleEnum mongodb权限
* @param dbName 数据库名(用户获取连接中的数据库名以及赋予数据库权限)
* @return
*/
public static boolean createUser(String mongoClient,String userName,String password, MongoDbRoleEnum roleEnum,String dbName){
if(ValidateUtils.isEmpty(mongoClient) || ValidateUtils.isEmpty(dbName) || ValidateUtils.isEmpty(roleEnum) || ValidateUtils.isEmpty(dbName)){
return false;
}
try {
MongoClient mongoClients = MongoClients.create(mongoClient);
Document doc = createUSerDocument(userName, password, roleEnum, dbName);
mongoClients.getDatabase(dbName).runCommand(doc);
}catch (Throwable e) {
return false;
}
return true;
}
/**
* 创建用户并赋予权限
* @param mongoDatabase mongodb数据连接
* @param userName 用户名
* @param password 密码
* @param roleEnum mongodb权限
* @return
*/
public static boolean createUser(MongoDatabase mongoDatabase,String userName,String password, MongoDbRoleEnum roleEnum){
if(ValidateUtils.isEmpty(mongoDatabase) || ValidateUtils.isEmpty(userName) || ValidateUtils.isEmpty(roleEnum) || ValidateUtils.isEmpty(password)){
return false;
}
try {
Document doc = createUSerDocument(userName, password, roleEnum, mongoDatabase.getName());
mongoDatabase.runCommand(doc);
}catch (Throwable e) {
return false;
}
return true;
}
private static MongoDatabase createDb(String mongoClient, String dbName){
if(ValidateUtils.isEmpty(mongoClient) || ValidateUtils.isEmpty(dbName)){
return null;
}
MongoClient mongoClients = MongoClients.create(mongoClient);
MongoDatabase mongoDatabase = mongoClients.getDatabase(dbName);
return mongoDatabase;
}
private static void createCollection(MongoDatabase mongoDatabase,String collectionName){
if(ValidateUtils.isNotEmpty(mongoDatabase)){
List<String> collectionNames = new ArrayList<>();
mongoDatabase.listCollectionNames().forEach(name -> collectionNames.add(name));
if(!collectionNames.contains(collectionName)){
mongoDatabase.createCollection(collectionName);
}
}
}
private static Document createUSerDocument(String userName,String password, MongoDbRoleEnum roleEnum,String dbName){
Document doc = new Document();
Document roleDoc = new Document();
roleDoc.put("role", roleEnum.getValue());
roleDoc.put("db", dbName);
doc.put("createUser",userName);
doc.put("pwd",password);
doc.put("roles", Collections.singletonList(roleDoc));
return doc;
}
}
9.编写mongodb枚举
public enum MongoDbRoleEnum {
/**
* 数据库用户角色
*/
READ("read","只读"),
READ_WRITE("readWrite","读写"),
/**
* 数据库管理角色
*/
DB_ADMIN("dbAdmin","一些数据库对象的管理操作,但是没有数据库的读写权限"),
DB_OWNER("dbOwner","该数据库的所有者,具有该数据库的全部权限"),
USER_ADMIN("userAdmin","为当前用户创建、修改用户和角色。拥有userAdmin权限的用户可以将该数据库的任意权限赋予任意的用户。"),
/**
* 集群管理权限
*/
CLUSTER_ADMIN("clusterAdmin","提供了最大的集群管理功能。相当于clusterManager, clusterMonitor, and hostManager和dropDatabase的权限组合。"),
CLUSTER_MANAGER("clusterManager","提供了集群和复制集管理和监控操作。拥有该权限的用户可以操作config和local数据库(即分片和复制功能)"),
CLUSTER_MONITOR("clusterMonitor","仅仅监控集群和复制集。"),
HOST_MANAGER("hostManager","提供了监控和管理服务器的权限,包括shutdown节点,logrotate, repairDatabase等。"),
/**
* 所有数据库角色
*/
READ_ANY_DATABASE("readAnyDatabase","具有read每一个数据库权限。但是不包括应用到集群中的数据库"),
READ_WRITE_ANY_DATABASE("readWriteAnyDatabase","具有readWrite每一个数据库权限。但是不包括应用到集群中的数据库"),
USER_ADMIN_ANY_DATABASE("userAdminAnyDatabase","具有userAdmin每一个数据库权限,但是不包括应用到集群中的数据库"),
DB_ADMIN_ANY_DATABASE("dbAdminAnyDatabase","提供了dbAdmin每一个数据库权限,但是不包括应用到集群中的数据库");
private String value;
private String desc;
MongoDbRoleEnum(String value,String desc){
this.desc = desc;
this.value = value;
}
public String getValue() {
return value;
}
public String getDesc() {
return desc;
}
}