目录
环境
jdk1.8
mongoDB 4.4.2
mongo环境CentOS8
mongo安装
官网地址:https://www.mongodb.com/try/download/community
复制链接(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的增删改查
目录结构
自定义注解
@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);
}
}
运行结果
截取了一个测试类的结果
总结
1、难度
api的风格与Spring Data Jpa类似,稍微练练应该可以就上手了。
如果业务只是普通的增删改查,看看菜鸟教程即可。
想要深入了解可以看看书,个人推荐MongoDB权威指南。
2、坑
@Document注解可以指定集合名称,根据需求加在实体类上,不过有两个属性长得很像,容易看错
collection
collation
3、为什么存到mongo中默认的集合名是实体类的类名?
我们可以简单看一下源码
一路点进去,可以找到这个类
如果实体类加上了注解,就是用注解中指定的集合名,如果没有,则是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);
}
至于它依赖的函数就不贴出了,有兴趣的可以研究。