目录

环境

mongo安装

整合SpringBoot

maven依赖

实现思路

目录结构

自定义注解

AOP

mongo服务

实体类

两个控制器

配置类

启动运行

MongoDB测试类

运行结果

总结


环境

jdk1.8

mongoDB 4.4.2

mongo环境CentOS8

mongo安装

官网地址:https://www.mongodb.com/try/download/community

springboot整合mongodb存储图片 springboot整合mongodb查询_spring

复制链接(Copy Link)

打开linux终端,进入 /usr/lcoal目录,创建文件夹mongo

wget + 刚才复制的链接,下载安装包

wget https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-rhel80-4.4.2.tgz

解压

tar -zxvf mongodb-linux-x86_64-rhel80-4.4.2.tgz

改名

mv mongodb-linux-x86_64-rhel80-4.4.2 mongodb

添加环境变量,永久生效

vim /etc/profile

将bin目录加进PATH

export PATH=/usr/local/mongo/mongodb/bin:$PATH

保存退出,立即生效。

source /etc/profile

创建依赖文件夹

(如果不是root用户,需要用管理员权限,并且设置允许当前用户访问该文件夹)

mkdir -p /var/lib/mongo
mkdir -p /var/log/mongodb

启动服务(建议方式二)

#方式一:仅本地使用,禁止远程连接
 mongod --dbpath /var/lib/mongo --logpath /var/log/mongodb/mongod.log --fork
#方式二:允许远程访问
mongod --dbpath /var/lib/mongo --logpath /var/log/mongodb/mongod.log --bind_ip=0.0.0.0  --fork

可以运行自带的客户端,查看是否安装成功

mongo

整合SpringBoot

maven依赖

<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-thymeleaf</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-mongodb</artifactId>
		</dependency>
		<dependency>
			<groupId>org.assertj</groupId>
			<artifactId>assertj-core</artifactId>
			<version>3.18.1</version>
		</dependency>
		<dependency>
			<groupId>org.aspectj</groupId>
			<artifactId>aspectjweaver</artifactId>
			<version>1.9.6</version>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<optional>true</optional>
		</dependency>
		<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>

配置文件

如果跟着上面的教程走下来,默认应该是没有密码的(可根据个人需要设置密码)。

server:
  port: 8080
spring:
  data:
    mongodb:
      uri: mongodb://ip:27017/oplog

实现思路

1、自定义注解,来标识需要监控的模块。

2、利用Spring的AOP来切面拦截需要记录的模块,mongoDB实现数据存取。

3、随便写两个controller,postman调用一下相关接口,产生测试数据。

4、测试mongoDB api的增删改查

目录结构

springboot整合mongodb存储图片 springboot整合mongodb查询_mongodb_02

自定义注解

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited //如果有子类,子类也可以获取到该类的注解信息
@Documented 
public @interface Operation {
    String name() default "未标注";
}

AOP

@Component
@Aspect
public class OperationAop {

    @Autowired
    OperationHistoryService operationHistoryService;

    //拦截controller里面的每个方法
    @Pointcut("execution(* com.dayrain.mongotest.controller.*.*(..))")
    public void method() {
    }

    @After("method()")
    public void after(JoinPoint joinPoint) {
    }

    @AfterReturning("method()")
    public void afterReturning(JoinPoint joinPoint) {
        //获取字节码对象
        Class<?> clazz = joinPoint.getTarget().getClass();

        if (clazz.isAnnotationPresent(Operation.class)) {
            //如果该类加了注解,则记录操作日志
            String modelName = clazz.getAnnotation(Operation.class).name();

            MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
            Method method = methodSignature.getMethod();
            if (method.isAnnotationPresent(Operation.class)) {
                //方法名
                String methodName = method.getDeclaredAnnotation(Operation.class).name();

                //获取方法参数
                Object[] args = joinPoint.getArgs();
                StringBuilder agrsRecord = new StringBuilder();
                for (Object arg : args) {
                    if (arg != null) {
                        agrsRecord.append(arg.toString());
                    }
                }
                //获取ip
                HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
                String ip = getRemoteHost(request);

                //这里随机生成一个用户,项目中获取当前登录用户
                HashMap<Integer, String> users = getUser();
                Random random = new Random();

                Integer current = random.nextInt(5) + 1;

                OperationHistory operationHistory = new OperationHistory();
                operationHistory.setIp(ip);
                operationHistory.setCreateTime(new Date());
                operationHistory.setMethodName(methodName);
                operationHistory.setModuleName(modelName);
                operationHistory.setParams(agrsRecord.toString());
                operationHistory.setUserId(String.valueOf(current));
                operationHistory.setUsername(users.get(current));
                operationHistoryService.saveLog(operationHistory);
            }
        }
    }

    private String getRemoteHost(HttpServletRequest request) {
        // 获取请求主机IP地址,如果通过代理进来,则透过防火墙获取真实IP地址
        String ip = request.getHeader("X-Forwarded-For");
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
                ip = request.getHeader("Proxy-Client-IP");
            }
            if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
                ip = request.getHeader("WL-Proxy-Client-IP");
            }
            if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
                ip = request.getHeader("HTTP_CLIENT_IP");
            }
            if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
                ip = request.getHeader("HTTP_X_FORWARDED_FOR");
            }
            if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
                ip = request.getRemoteAddr();
            }
        } else if (ip.length() > 15) {
            String[] ips = ip.split(",");
            for (String s : ips) {
                if (!("unknown".equalsIgnoreCase((String) s))) {
                    ip = s;
                    break;
                }
            }
        }
        return ip;
    }

    //用户信息,项目中应该获取当前登录用户
    private HashMap<Integer, String> getUser() {
        HashMap<Integer, String> users = new HashMap<>();
        users.put(1, "刘德华");
        users.put(2, "赵本山");
        users.put(3, "成龙");
        users.put(4, "周星驰");
        users.put(5, "成龙");

        return users;
    }
}

mongo服务

@Service
public class OperationHistoryService {
    @Autowired
    private MongoTemplate mongoTemplate;

    /**
     * 保存操作日志
     * @param operationHistory 操作日志
     */
    public void saveLog(OperationHistory operationHistory) {
        mongoTemplate.save(operationHistory);
    }

    /**
     * 根据用户id查找操作日志
     * @param userId 用户id
     * @return
     */
    public List<OperationHistory> findByUserId(String userId) {
        Query query = new Query(Criteria.where("userId").is(userId));
        return mongoTemplate.find(query, OperationHistory.class);
    }
}

实体类

@Data
public class OperationHistory {
    private String ip;

    private String userId;

    private String username;

    private String moduleName;

    private String methodName;

    private String params;

    private Date createTime;
}

两个控制器

@Operation(name = "消费模块")
@RestController
@RequestMapping("/custom")
public class CustomController {

    @Operation(name = "支付")
    @RequestMapping("/pay")
    public String pay() {
        return "pay";
    }

    @Operation(name = "查找订单")
    @RequestMapping("/order")
    public String order(String id) {
        return "order " + id;
    }
}
@Operation(name = "物资模块")
@RestController
@RequestMapping("/test")
public class TestController {

    @Operation(name = "添加物资")
    @RequestMapping("/add")
    public String add() {
        return "add";
    }

    @Operation(name = "删除物资")
    @RequestMapping("/delete")
    public String delete(String id) {
        return "delete " + id;
    }

    @Operation(name = "更新物资")
    @RequestMapping("/update")
    public String update() {
        return "update";
    }

    @Operation(name = "查找物资")
    @RequestMapping("/select")
    public String select() {
        return "select";
    }
}

配置类

配置类主要是解决跨域的,可选

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**").allowedOrigins("*")
                .allowedHeaders("*")
                .allowedMethods("*")
                .maxAge(30*1000);
    }
}

启动运行

运行SpringBoot启动类,访问接口,产生测试数据。

MongoDB测试类

mongo  API练习

@SpringBootTest
class MongoTestApplicationTests {

	@Autowired
	MongoTemplate mongoTemplate;
	//查询所有
	@Test
	public void testSelect() {
		List<OperationHistory> all = mongoTemplate.findAll(OperationHistory.class);
		//第二个参数可以设置集合命,默认是实体类的类名,例如OperationHistory的集合命就是operationHistory
		//List<OperationHistory> all = mongoTemplate.findAll(OperationHistory.class, "集合名");
		all.forEach(System.out::println);
	}

	//条件查询
	@Test
	public void conditionQuery() {
		//查询id为6的用户
		Query query = new Query(Criteria.where("userId").is("5"));
		List<OperationHistory> operationHistories = mongoTemplate.find(query, OperationHistory.class);
		operationHistories.forEach(System.out::println);
	}

	//更新
	@Test
	public void update() {
		Query query = new Query(Criteria.where("username").is("成龙"));
		Update update = new Update().set("username", "周杰伦");
		//只更新第一个
		//UpdateResult updateResult = mongoTemplate.updateFirst(query, update, OperationHistory.class);
		//更新所有
		UpdateResult updateResult = mongoTemplate.updateMulti(query, update, OperationHistory.class);
		System.out.println("一共修改了"+ updateResult.getModifiedCount() +"条");
	}

	//删除
	@Test
	public void delete() {
		//全部删除不返回
		Query query = new Query(Criteria.where("username").is("刘德华"));
		mongoTemplate.remove(query, OperationHistory.class);

		//删除一条,并返回被删除的对象,如果该对象不存在,则返回一个空串
		Query query2 = new Query(Criteria.where("username").is("周星驰"));
		OperationHistory operationHistory = mongoTemplate.findAndRemove(query2, OperationHistory.class);
		System.out.println(operationHistory);

		//删除多条,并返回被删除的对象,但是这个方法要求实体类必须有_id,否则会报错,我们这个案例中没有指定_id,用的是mongo自动生成的。所以没办法使用这个方法
		Query query3 = new Query(Criteria.where("username").is("周星驰"));
		List<OperationHistory> res1 = mongoTemplate.findAllAndRemove(query2, OperationHistory.class);
		res1.forEach(System.out::println);

	}
}

运行结果

截取了一个测试类的结果

springboot整合mongodb存储图片 springboot整合mongodb查询_mongodb_03

总结

1、难度

api的风格与Spring Data Jpa类似,稍微练练应该可以就上手了。

如果业务只是普通的增删改查,看看菜鸟教程即可。

想要深入了解可以看看书,个人推荐MongoDB权威指南。

2、坑

@Document注解可以指定集合名称,根据需求加在实体类上,不过有两个属性长得很像,容易看错

collection
collation

3、为什么存到mongo中默认的集合名是实体类的类名?

我们可以简单看一下源码

springboot整合mongodb存储图片 springboot整合mongodb查询_aop_04

一路点进去,可以找到这个类

springboot整合mongodb存储图片 springboot整合mongodb查询_mongodb_05

如果实体类加上了注解,就是用注解中指定的集合名,如果没有,则是fallback。

点进fallback,找到他的生成函数

public static String getPreferredCollectionName(Class<?> entityClass) {
		return StringUtils.uncapitalize(entityClass.getSimpleName());
	}

这个getSImpleName函数有着详细的命名转化规则。推荐项目中还是自己指定一个吧。

public String getSimpleName() {
        if (isArray())
            return getComponentType().getSimpleName()+"[]";

        String simpleName = getSimpleBinaryName();
        if (simpleName == null) { // top level class
            simpleName = getName();
            return simpleName.substring(simpleName.lastIndexOf(".")+1); // strip the package name
        }
        // According to JLS3 "Binary Compatibility" (13.1) the binary
        // name of non-package classes (not top level) is the binary
        // name of the immediately enclosing class followed by a '$' followed by:
        // (for nested and inner classes): the simple name.
        // (for local classes): 1 or more digits followed by the simple name.
        // (for anonymous classes): 1 or more digits.

        // Since getSimpleBinaryName() will strip the binary name of
        // the immediatly enclosing class, we are now looking at a
        // string that matches the regular expression "\$[0-9]*"
        // followed by a simple name (considering the simple of an
        // anonymous class to be the empty string).

        // Remove leading "\$[0-9]*" from the name
        int length = simpleName.length();
        if (length < 1 || simpleName.charAt(0) != '$')
            throw new InternalError("Malformed class name");
        int index = 1;
        while (index < length && isAsciiDigit(simpleName.charAt(index)))
            index++;
        // Eventually, this is the empty string iff this is an anonymous class
        return simpleName.substring(index);
    }

至于它依赖的函数就不贴出了,有兴趣的可以研究。