基于Spring AOP的日志功能实现
前言
前一段时间学习了Spring,明确Spring的两大特征:IoC控制反转和AOP面向切面编程。后续遇到了系统日志功能,实现的时候使用到了AOP,在此进行总结。
IoC:主要是将程序中的对象通过创建bean对象的方式将其加入到Spring容器中,通过依赖注入的方式调用容器中的bean对象,从而降低程序间的依赖性(传统是通过new 方式获取类对象)
AOP:面向切面编程,抽取出程序中重复度较高的代码,然后项目中哪里需要使用,就通过反向代理的方式调用这部分重复度高的代码,实现原功能的代码增强。
参考链接
系统操作日志的实现:
https://blog.csdn.net/t_jindao/article/details/85259145
https://blog.csdn.net/Danny1992/article/details/103684567
AOP知识点:
https://baike.baidu.com/item/AOP/1332219?fr=aladdin
https://www.jianshu.com/p/5b9a0d77f95f
AOP介绍
详细的基于XML实现和基于注解实现可查看此篇博客知识点中的第三点AOP介绍:https://blog.csdn.net/qq_38586378/article/details/107775845
AOP的概念
Aspect Oriented Programming,面向切面编程。是OOP的延续,利用AOP可以对业务逻辑的各个部分的重复代码抽取出来,在需要执行的时候通过动态代理的技术,在不修改源码的基础上,对已有的方法进行增强。
AOP的优势在于减少重复代码;提高开发效率;便于工程项目维护
AOP的相关术语
1. Target目标类
指代理的目标对象即被代理对象
2. Proxy代理
一个类被AOP织入增强后,就产生一个结果代理类(代理对象)
3. Jointpoint连接点
指被拦截到的点,具体来讲就是项目中的方法
4. Adive增强/通知
指拦截到连接点后需要做的事情,一般会进行增强的连接点为称之为切入点。分为在切入点执行前的通知、在切入点执行正常的通知、执行异常的通知、执行完毕的通知。
4.1 前置通知
切入点方法执行之前的操作。xml中为aop:before,基于注解中是@Before()
4.2 后置通知
切入点方法执行之后的操作。xml中为aop:returning,基于注解中是@AfterReturning()
4.3 异常通知
类似catch操作。xml中为aop:throwing,基于注解中是@AfterThrowing()
4.4 最终通知
类似finally操作。xml中为aop:after,基于注解中是@After()
4.5 环绕通知
xml中是aop:around,基于注解中是@Around。
由于spring框架对于前置-后置/异常-最终顺序没有规定,所以可能处理的时候不会符合用户的执行顺序需求,可使用环绕通知,手动插入需要在切入点之前/之后执行的操作,保证切面增强处理逻辑的顺序正确性
5. Weaving织入
指把增强应用到目标对象来创建新的代理对象的过程(通过aop将通知织入到切入点中,实现代码增强)
6. Introduction引入
一种特殊的通知在不修改类代码的前提下,Introduction可在运行期为类动态地添加一些方法或者Field变量
7. Aspect切面
切入点和通知/引介的结合,一般会在切面中配置落实到增强某个切入点的通知
AOP基于XML的环绕通知实现
1. pom.xml导入AOP依赖
<!-- aop依赖 -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.7</version>
</dependency>
2. 编写Spring配置文件bean.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- 配置spring的Ioc,把service对象配置进来 -->
<bean id="accountService" class="com.practice.service.impl.AccountServiceImpl"></bean>
<!-- spring中基于XML的AOP配置步骤
1. 把通知Bean也交给spring来管理
2. 使用aop:config标签表明开始AOP的配置
3. 使用aop:aspect标签表明配置切面
id属性:是给切面提供一个唯一标识
ref属性:是指定通知类bean的Id
4. 在aop:aspect标签的内部使用对应标签来配置通知的类型
现在示例是让printLog方法在切入点方法执行之前执行:所以为前置通知
aop:before 标识配置前置通知
method属性:用于指定Logger类中哪个方法是前置的
pointcut属性:用于指定切入点表达式,该表达式的含义指的是对业务层中的哪些方法增强
切入点表达式的写法:
关键字:execution(表达式)
表达式:
访问修饰符 返回值 包名.包名.包名...类名.方法名(参数列表)
标准的表达式写法:
public void com.practice.service.impl.AccountServiceImpl.saveAccount()
访问修饰符可以省略
void com.practice.service.impl.AccountServiceImpl.saveAccount()
返回值可以使用通配符,表示任意返回值
* com.practice.service.impl.AccountServiceImpl.saveAccount()
包名可以使用通配符,表示任意包。但是有几级包,就需要写几个*
* *.*.*.*.AccountServiceImpl.saveAccount()
包名可以使用..表示当前包及其子包
* *..AccountServiceImpl.saveAccount()
类名和方法名都可以使用*来实现通配
* *..*.*()
参数列表:
可以直接写数据类型
基本类型直接写名称 int
引用类型写包名.类名的方式 java.lang.String
可以使用通配符表示任意类型,但必须有参数
可以使用..表示有无参数均可,有参数可以是任意类型
全通配写法:
* *..*.*(..)
实际开发中切入点表达式的通常写法:
切到业务层实现类下的所有方法
* com.practice.service.impl.*.*(..)
-->
<!-- 配置Logger类 -->
<bean id="logger" class="com.practice.utils.Logger"></bean>
<!-- 配置AOP -->
<aop:config>
<!-- 配置切入点表达式:id属性用于执行表达式的唯一标识,expression属性用于指定表达式内容
此标签写在aop:aspect标签内部只能当前切面aspect使用
也可写在aop:aspect标签外面供所有的切面使用
-->
<aop:pointcut id="pt1" expression="execution(* com.practice.service.impl.*.*(..))"/>
<!-- 配置切面 -->
<aop:aspect id="logAdvice" ref="logger">
<!-- 配置通知的类型,并且建立通知方法和切入点方法的关联 -->
<!-- 配置环绕通知 -->
<aop:around method="aroundPrintLog" pointcut-ref="pt1"></aop:around>
</aop:aspect>
</aop:config>
</beans>
package com.practice.utils;
/**
* @ClassName Logger
* @Author wx
* @Date 2020/7/22 0022 20:53
* @Version 1.0
* @Attention Copyright (C), 2004-2020, BDILab, XiDian University
**/
import org.aspectj.lang.ProceedingJoinPoint;
/**
* 用户记录日志的工具类,里面提供公共的代码
*/
public class Logger {
/**
* 环绕通知
* 问题:
* 当配置环绕通知之后,切入点方法没有执行,而通知方法被执行了
* 分析:
* 通过对比动态代理中的环绕通知代码,发现动态代理中的环绕通知有明确的切入点方法调用,而现在的代码中没有
* 解决:
* Spring框架提供了一个借口,ProceedingJoinPoint。该接口有一个方法proceed(),此方法相当于明确调用切入点方法,
* 该接口可作为环绕通知的方法参数,在程序执行时spring框架会提供该接口的实现类供我们使用
*
* spring中的环绕通知:
* 是spring框架为我们提供的一种可以在代码中手动控制增强方法何时执行的方式
*/
public Object aroundPrintLog(ProceedingJoinPoint joinPoint){
Object returnValue = null;
try{
Object[] args = joinPoint.getArgs();
System.out.println("beforePrintLog...");
returnValue = joinPoint.proceed(args); //明确调用业务层方法(切入点方法)
System.out.println("afterReturningPrintLog...");
return returnValue;
}catch (Throwable t){
System.out.println("afterThrowingPrintLog...");
throw new RuntimeException(t);
}finally {
System.out.println("afterPrintLog...");
}
}
}
AOP基于注解的环绕通知实现
1. pom.xml导入依赖
<!-- aop依赖 -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.7</version>
</dependency>
2. 在spring配置文件bean.xml中开启注解扫描
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<!-- 配置spring创建容器时要扫描的包 -->
<context:component-scan base-package="com.practice"></context:component-scan>
<!-- 配置spring开启注解AOP的支持 -->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>
3. 增强类中添加注解,并编写切入点和通知方法
package com.practice.utils;
/**
* @ClassName Logger
* @Author wx
* @Date 2020/7/22 0022 20:53
* @Version 1.0
* @Attention Copyright (C), 2004-2020, BDILab, XiDian University
**/
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
/**
* 用户记录日志的工具类,里面提供公共的代码
*/
@Component("logger")
@Aspect //表示当前类是一个切面类
public class Logger {
@Pointcut("execution(* com.practice.service.impl.*.*(..))")
private void pt1(){}
@Around("pt1()")
public Object aroundPrintLog(ProceedingJoinPoint joinPoint){
Object returnValue = null;
try{
Object[] args = joinPoint.getArgs();
System.out.println("beforePrintLog...");
returnValue = joinPoint.proceed(args); //明确调用业务层方法(切入点方法)
System.out.println("afterReturningPrintLog...");
return returnValue;
}catch (Throwable t){
System.out.println("afterThrowingPrintLog...");
throw new RuntimeException(t);
}finally {
System.out.println("afterPrintLog...");
}
}
}
4. 如果需要使用全注解的话,可以在项目启动类上加@EnableAspectJAutoProxy开启注解AOP扫描(加@ComponentScan开启bean扫描)
功能实现过程
需求
一个JavaWeb项目中,涉及例如用户管理、系统管理等模块,现在需要记录每次用户操作,并可查询用户的操作日志。
实现逻辑
其实想想这个功能不难,无非就是在每次操作的时候,生成一条操作日志插入到数据表中,然后提供查询日志的接口即可。但是如果单纯把生成日志对象+往日志表中查数据的代码放在每个方法中,未免过于冗余,而且也很不优雅。
想想其实就是在每个操作的基础上添加一个功能,各个模块的功能有需要有这个增强功能,多多少少是个横向的意思,那么可以使用spring提供的AOP功能,将需要生成日志的方法作为切入点,然后编写增强代码即生成日志和插入日志的部分即可。
设计
操作日志存到数据库中,日志表的字段主要有自增id、用户名、操作方法、url、操作时间等。
代码实现
利用mybatis-plugin生成domain mapper和mapper.xml文件
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
"http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>
<!-- 导入数据库配置文件 -->
<properties resource="jdbc.properties"></properties>
<!-- context:****的主要配置信息
属性:
id:名称
targetRuntime:设置生成的文件适用于哪个mybatis版本,默认MyBatis3-->
<context id="study" targetRuntime="MyBatis3">
<!-- 可选的,用于创建class的时候对注释进行控制 -->
<commentGenerator>
<!-- 去除指定生成的注释中是否包含生成的日期,true表示去除日期 -->
<property name="suppressDate" value="true"/>
<!-- 是否去除自动生成的注释,true表示去除 -->
<property name="suppressAllComments" value="true"/>
</commentGenerator>
<!-- 数据库JDBC连接 -->
<jdbcConnection driverClass="${jdbc.driver}"
connectionURL="${jdbc.url}"
userId="${jdbc.username}"
password="${jdbc.password}">
<property name="nullCatalogMeansCurrent" value="true"/>
</jdbcConnection>
<javaTypeResolver>
<property name="forceBigDecimals" value="false"/>
</javaTypeResolver>
<!-- 配置反向自动生成的实体类
targetPackage:生成的实体类所在的包
targetProject:生成的实体类所在的硬盘位置-->
<javaModelGenerator targetPackage="com.practice.mall.domain" targetProject="src/main/java">
<!-- 是否允许子包,false表示不允许-->
<property name="enableSubPackages" value="false"/>
<!-- 是否清理从数据库中查询出的字符串左右两边的空白字符,true是 -->
<property name="trimStrings" value="true"/>
<!-- 是否对modal添加构造函数 -->
<!-- <property name="constructorBased" value="true"/>-->
<!-- 建立modal对象是否不可改变 即生成的modal对象不会有setter方法,只有构造方法 -->
<!-- <property name="immutable" value="false"/>-->
</javaModelGenerator>
<!-- 为防止后续使用generator覆盖掉mapper和mapper.xml,所以一般会初始生成后直接注释掉mapper类和mapper.xml,只留下domain的自动生成-->
<!-- 配置反向自动生成的mapper.xml文件-->
<!-- <sqlMapGenerator targetPackage="mapper" targetProject="src/main/resources">-->
<!-- <!– 针对数据库的一个配置,是否把 schema 作为字包名 –>-->
<!-- <property name="enableSubPackages" value="false"/>-->
<!-- </sqlMapGenerator>-->
<!-- <!– 配置反向自动生成的mapper类 –>-->
<!-- <javaClientGenerator type="XMLMAPPER" targetPackage="com.practice.mall.mapper" targetProject="src/main/java">-->
<!-- <!– 针对 oracle 数据库的一个配置,是否把 schema 作为字包名 –>-->
<!-- <property name="enableSubPackages" value="false"/>-->
<!-- </javaClientGenerator>-->
<!-- 配置生成的表 tableName是数据库中的表名,domainObjectName是生成的JAVA模型名
tableName(必要):要生成对象的表名 -->
<table tableName="sys_log" domainObjectName="SysLog" enableCountByExample="false" enableUpdateByExample="false" enableDeleteByExample="false" enableSelectByExample="false" selectByExampleQueryId="false"></table>
</context>
</generatorConfiguration>
这里需要注意一点是,如果一个项目中有多个模块,后续添加某个表旧一定要注释掉配置文件中的自动生成mapper和mapper.xml文件,否则会将之前手动添加的方法覆盖掉(当然也可以配置不要覆盖)
通过配置前置通知和后置通知实现日志:
@Component
@Aspect
public class LogAop {
@Autowired
HttpServletRequest request;
@Autowired
SysLogService sysLogService;
private Date visitTime; //开始时间
private Class aClass; //访问的类
private String methodName; //访问的方法名
private String username; //用户名
//前置通知,主要获取开始时间、执行的类是哪个、执行的是哪一个方法(对于切入点中的路径根据实际项目中的路径编写)
@Before("execution(* com.practice.mall.controller.*.*.*(..))")
public void doBefore(JoinPoint joinPoint) throws NoSuchMethodException {
visitTime = new Date(); //当前时间即开始访问的时间
aClass = joinPoint.getTarget().getClass(); //具体要访问的类
methodName = joinPoint.getSignature().getName(); //获取访问的方法的名称
//例如登录功能,需要在切入点执行前获取用户名;功能执行之后HttpSession中无法获取用户名
if(request.getSession().getAttribute(Const.CURRENT_USER)!=null){
username = ((MallUser)request.getSession().getAttribute(Const.CURRENT_USER)).getUsername();
}
}
//后置通知 execution中的第一个*表示任意方法返回值 第二个*表示backend or portal 第三个*表示xxxController 第四个*表示xxxcontroller中的方法
@After("execution(* com.practice.mall.controller.*.*.*(..))")
public void doAfter(JoinPoint joinPoint){
long time = new Date().getTime() - visitTime.getTime(); //获取访问时长
//获取url
String url = "";
if(aClass != null && methodName != null && !StringUtils.equals(methodName,"/listSysLog") && aClass != LogAop.class){ //根据实际需求可过滤掉不需要记录到日志中的method
//利用反射获取类上的@RequestMapping 注解也是一个类
//获取类上的value值
RequestMapping classAnnotation = (RequestMapping)aClass.getAnnotation(RequestMapping.class);
if(classAnnotation != null){
String[] classValue = classAnnotation.value();
url = classValue[0] + "/" + methodName;
//获取访问的ip
String ip = request.getRemoteAddr();
if(StringUtils.isBlank(username) && request.getSession().getAttribute(Const.CURRENT_USER)!=null){
username = ((MallUser)request.getSession().getAttribute(Const.CURRENT_USER)).getUsername();
}else if(StringUtils.isBlank(username)){
username = "test";
}
//将日志相关信息封装到sysLog对象中
SysLog sysLog = new SysLog();
sysLog.setUsername(username);
sysLog.setVisitTime(visitTime);
sysLog.setIp(ip);
sysLog.setUrl(url);
sysLog.setMethod("[类名]" + aClass.getName() + "[方法名]" + methodName); //有时候aclass和method可能为null
sysLog.setExecutePeriod(time);
sysLogService.addSysLog(sysLog);
}
}
}
}
可以改为环绕通知方法实现
@Component
@Aspect
public class LogAop {
@Autowired
HttpServletRequest request;
@Autowired
SysLogService sysLogService;
private Date visitTime; //开始时间
private Class aClass; //访问的类
private String methodName;
private String username; //用户名
@Around("execution(* com.practice.mall.controller.*.*.*(..))")
public void around(ProceedingJoinPoint joinPoint){
//前置通知
visitTime = new Date(); //当前时间即开始访问的时间
aClass = joinPoint.getTarget().getClass(); //具体要访问的类
methodName = joinPoint.getSignature().getName(); //获取访问的方法的名称
if(request.getSession().getAttribute(Const.CURRENT_USER)!=null){
username = ((MallUser)request.getSession().getAttribute(Const.CURRENT_USER)).getUsername();
}
//执行切入点操作
try {
joinPoint.proceed(joinPoint.getArgs());
} catch (Throwable throwable) {
throwable.printStackTrace();
}
//后置通知
long time = new Date().getTime() - visitTime.getTime(); //获取访问时长
//获取url
String url = "";
if(aClass != null && methodName != null && !StringUtils.equals(methodName,"/listSysLog") && aClass != LogAop.class) { //根据实际需求可过滤掉不需要记录到日志中的method
//利用反射获取类上的@RequestMapping 注解也是一个类
//获取类上的value值
RequestMapping classAnnotation = (RequestMapping) aClass.getAnnotation(RequestMapping.class);
if (classAnnotation != null) {
String[] classValue = classAnnotation.value();
url = classValue[0] + "/" + methodName;
//获取访问的ip
String ip = request.getRemoteAddr();
if (StringUtils.isBlank(username) && request.getSession().getAttribute(Const.CURRENT_USER) != null) {
username = ((MallUser) request.getSession().getAttribute(Const.CURRENT_USER)).getUsername();
} else if (StringUtils.isBlank(username)) {
username = "test";
}
//将日志相关信息封装到sysLog对象中
SysLog sysLog = new SysLog();
sysLog.setUsername(username);
sysLog.setVisitTime(visitTime);
sysLog.setIp(ip);
sysLog.setUrl(url);
sysLog.setMethod("[类名]" + aClass.getName() + "[方法名]" + methodName); //有时候aclass和method可能为null
sysLog.setExecutePeriod(time);
sysLogService.addSysLog(sysLog);
}
}
}
}
优化
其实项目中并不是所有的controller方法都需要记录操作日志,如此处理未免有些莽撞,可以通过给controller层或者其他层的方法添加注解的方法,织入通知,完成日志的生成和表数据插入即可
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface SysLogAnnotation {
}
@Component
@Aspect
public class LogAop {
@Autowired
HttpServletRequest request;
@Autowired
SysLogService sysLogService;
private Date visitTime; //开始时间
private Class aClass; //访问的类
private String methodName;
private String username; //用户名
@Around(value = "@annotation(sysLogAnnotation)")
public void logAround(final ProceedingJoinPoint joinPoint, final SysLogAnnotation sysLogAnnotation){
//前置通知
visitTime = new Date(); //当前时间即开始访问的时间
aClass = joinPoint.getTarget().getClass(); //具体要访问的类
methodName = joinPoint.getSignature().getName(); //获取访问的方法的名称
if(request.getSession().getAttribute(Const.CURRENT_USER)!=null){
username = ((MallUser)request.getSession().getAttribute(Const.CURRENT_USER)).getUsername();
}
//执行切入点操作
try {
if(joinPoint.getArgs().length == 0){
joinPoint.proceed();
}else {
joinPoint.proceed(joinPoint.getArgs());
}
} catch (Throwable throwable) {
throwable.printStackTrace();
}
//后置通知
long time = new Date().getTime() - visitTime.getTime(); //获取访问时长
//获取url
String url = "";
if(aClass != null && methodName != null && !StringUtils.equals(methodName,"listSysLog")) { //根据实际需求可过滤掉不需要记录到日志中的method
//利用反射获取类上的@RequestMapping 注解也是一个类
//获取类上的value值
RequestMapping classAnnotation = (RequestMapping) aClass.getAnnotation(RequestMapping.class);
if (classAnnotation != null) {
String[] classValue = classAnnotation.value();
url = classValue[0] + "/" + methodName;
//获取访问的ip
String ip = request.getRemoteAddr();
if (StringUtils.isBlank(username) && request.getSession().getAttribute(Const.CURRENT_USER) != null) {
username = ((MallUser) request.getSession().getAttribute(Const.CURRENT_USER)).getUsername();
} else if (StringUtils.isBlank(username)) {
username = "test";
}
//将日志相关信息封装到sysLog对象中
SysLog sysLog = new SysLog();
sysLog.setUsername(username);
sysLog.setVisitTime(visitTime);
sysLog.setIp(ip);
sysLog.setUrl(url);
sysLog.setMethod("[类名]" + aClass.getName() + "[方法名]" + methodName); //有时候aclass和method可能为null
sysLog.setExecutePeriod(time);
sysLogService.addSysLog(sysLog);
}
}
}
}
然后在需要记录操作日志的方法上加@SysLogAnnotation注解即可,执行后数据库添加一条记录
当然如果不同的方法有自定义的一些字段例如涉及操作的数据库表名等信息,可以通过在注解中添加属性,然后从注解中获得。
具体参考:https://blog.csdn.net/t_jindao/article/details/85259145
测试
执行多个方法后的数据库日志表
也支持通过接口查询所有的操作日志
拓展
1. 注意如果调用环绕通知方法的时候报错:org.springframework.aop.AopInvocationException: Null return value from advice does not match primitive return type for....
主要是因为环绕方法的返回值为void,而实际需要增强的切入点方法返回值为object,返回值类型不匹配所以报错。(反向代理的主要过程就是先实现增强的部分,最后通过代理实现被代理的方法,所以一般而言被代理方法和代理方法的返回值需要保持一致),解决方法是将环绕方法的返回值由void改为Object即可(默认代理的时候会将Object根据切入点方法返回值类型的不同进行类型强制转换)。
参考链接:https://blog.csdn.net/fdk2zhang/article/details/82987497
2. 另外博客中的ip没有进行处理,对于ip的获取可以参考:https://blog.csdn.net/qq_36411874/article/details/79938439
/**
* @param request
* @return
*/
public static String getIpAddress(HttpServletRequest request) {
String ip = request.getHeader("x-forwarded-for");
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.getHeader("X-Real-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
if("0:0:0:0:0:0:0:1".equals(ip)){
ip = "127.0.0.1";
}
return ip;
}
3. 再拓展一个前端Get和Post请求的区别,参考:
https://blog.csdn.net/kelly0721/article/details/88415806
https://mp.weixin.qq.com/s/4_IQcjcrsRS0iZ2ze7nQRA
我自己的感觉是本质上对于获取数据没什么区别,表面上的区别是get是从服务器获取数据,post是发送数据到服务器中。
但实际对于需求功能而言无论是get和post都是发送数据到服务器,然后从服务器中拿取数据,只不过就是主体不一样。如果重点在于从服务器中拿取数据那么用get;如果重点在于发送数据到服务器,那么用post。另外对于一些文件上传,还有后端controller方法的参数加@RequestBody的时候,也使用post而非get。
一般对数据库做查询操作推荐使用get,对数据库做写操作(新增、更新、删除)推荐使用post,另外对于一些不想在url中展示参数可使用post。登录功能一般都使用的是post,是不是也是考虑到username和password安全性的问题呢?(目前浅显学习之后的一家之言,欢迎讨论~)
总结
学习的基本策略:基础概念、实战操作、底层原理以及日常结合其他方面的各种练习总结。