Ykuri98
文章46
标签14
分类1
学习总结(2022.06.15-2022.06.24)

学习总结(2022.06.15-2022.06.24)

动态代理

JDK动态代理

JDK动态代理是基于Java的反射机制实现的,使用jdk中的接口和类实现代理对象的动态创建,但是要求代理对象必须实现接口。

JDK动态代理的实现步骤如下。

  1. 新建一个接口,作为目标接口。
1
2
3
4
public interface UserService {

public void sayHello();
}
  1. 为接口创建实现类。
1
2
3
4
5
6
7
8
public class UserServiceImpl implements UserService{

@Override
public void sayHello() {
System.out.println("hello spring");
}

}
  1. 创建一个类实现InvocationHandler接口及其invoke方法,在method.invoke()前后执行对方法的增强。
1
2
3
4
5
6
public class CustomInvocationHandler implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
return null;
}
}
  1. 在main方法中调用Proxy.newProxyInstance()创建JDK动态代理,使用代理对象执行方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Test
public void mytest4() {
UserService instance = new UserServiceImpl(); // instance 👉 method.invoke(instance,args);
// UserServiceImpl 👉 sayHello
// Proxy 👉 sayHello
UserService userServiceProxy = (UserService) Proxy.newProxyInstance(UserServiceImpl.class.getClassLoader(),
UserServiceImpl.class.getInterfaces(),
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//jdsfkhjasdjyr
System.out.println("jdsfkhjasdjyr");
Object invoke = method.invoke(instance, args);
//dfjkasdhjfisdya
System.out.println("dfjkasdhjfisdya");
return invoke;
}
});
userServiceProxy.sayHello();//invocationHandler.invoke
}

Cglib动态代理

Cglib动态代理只针对类实现代理,不需要实现接口。

Cglib动态代理的实现步骤如下。

  1. 创建一个类实现MethodInterceptor接口及其intercept方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class MyMethodInterceptor implements MethodInterceptor {
/**
* cglib
*
* @param o cglib生成的代理对象
* @param method 被代理对象的方法
* @param objects 方法入参
* @param methodProxy 代理方法
* @return
* @throws Throwable
*/
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println("before advice...");
Object object = methodProxy.invokeSuper(o, objects);
System.out.println("after advice...");
return object;
}
}
  1. 在main方法中,通过Enhancer获取代理对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Main {
public static void main(String[] args) {
//通过CGLIB动态代理获取代理对象的过程
Enhancer enhancer = new Enhancer();
//设置enhancer对象的父类
enhancer.setSuperclass(HelloService.class);
//设置enhancer的回调对象
enhancer.setCallback(new MyMethodInterceptor());
//创建代理对象
HelloService proxy = (HelloService) enhancer.create();
//通过代理对象调用目标方法
proxy.sayHello();
}
}

IOC/DI

IOC,控制反转,将生成实例的控制权交由Spring容器,容器只控制实例的生成,而开发人员只需要往容器中注册需要生成实例的类,即组件,并在需要时取出该组件的对象即可。

DI,依赖注入,即具体的IOC实现方式。一般通过配置文件和注解来完成依赖注入。

XML

在Spring的XML文件中注册组件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!--注册组件 → 提供信息 → 向容器中放入实例-->
<!--id属性:组件在容器中的唯一标识 → 可以省略-->
<!--class属性:组件的全限定类名-->
<bean id="userDao" class="com.cskaoyan.dao.UserDaoImpl"/>

<!--set方法来维护组件之间的关系-->
<bean id="userService" class="com.cskaoyan.service.UserServiceImpl">
<!--property标签的name属性 → set方法-->
<!--ref属性:reference → 容器中的组件id-->
<property name="userDao" ref="userDao"/>
</bean>

<bean id="orderService" class="com.cskaoyan.service.OrderServiceImpl">
<property name="userDao" ref="userDao"/>
</bean>

取出组件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Test
public void mytest1() {
// 初始化Spring容器
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("application.xml");

// 通过id取出组件
UserDao userDao = (UserDao) applicationContext.getBean("userDao");
// 通过类型取出组件byType → class 可以写接口、实现类 → 该类型的组件在容器中只有一个
// 建议用接口
UserService userService1 = applicationContext.getBean(UserService.class);
UserService userService2 = applicationContext.getBean(UserServiceImpl.class);

// 通过id和类型共同取出
OrderService orderService = applicationContext.getBean("orderService", OrderService.class);
}

注解

也可以使用注解注册组件。可以在注解中指定组件id,也可以使用默认的组件id,默认id一般为类名的首字母小写。

1
2
3
4
5
6
7
8
9
10
11
// 通用注解
@Component

// service层
@Service

// dao层
@Repository

// controller层
@Controller

配置类

在一个类上加上@Configuration注解,表示该类被注册为一个组件,且该类为一个配置类。类中的方法可以被@Bean注解修饰,表示该方法的返回值的实例会被注册为一个组件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Configuration
// @ComponentScan注解表示扫描指定包下的所有类,并将带有@Component等相关注解的类注册为组件
@ComponentScan("com.cskaoyan")
public class SpringConfiguration {

/**
* 返回值类型:Spring容器中的组件类型
* 返回值:返回值注册为容器中的组件
* 组件ID:默认值是方法名;如果想要指定组件id,可以使用@Bean的value属性
*/
@Bean("druidDatasource")
public DataSource dataSource() {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:3306/cskaoyan_db");
dataSource.setUsername("root");
dataSource.setPassword("123456");

return dataSource;
}
/**
* 形参:默认按照类型从容器中取出组件;想要指定组件id → @Qualifier
*/
// 注册一个UserServiceImpl组件
@Bean
public UserService userService(@Qualifier("userDaoImpl") UserDao userDao) {
UserServiceImpl userService = new UserServiceImpl();
// 从容器中取出userDao并且给其赋值
userService.setUserDao(userDao);
return userService;
}
}

@Autowired

类中的成员变量可以通过@Autowired注解注册为组件,此时该成员变量就不需要开发人员手动初始化。@Autowired的本质是利用容器提供的set方法。

1
2
3
4
@Autowired
// 如果该类型的组件不止一个,需要使用@Qualifier来指定组件的id
@Qualifier("userDaoImpl2")
UserDao userDao;

组件的生命周期

组件可以使用@Scope注解来确定自己的作用域。如果作用域为singleton,即单例模式,那么每次从容器中取出的组件都是同一个组件;如果使用的是prototype,即原型模式,那么每次取出的都是新的实例。Spring默认使用单例模式。

在单例模式下,容器初始化时就会开始组件的生命周期;如果是原型模式,则只有在获得组件时才开始生命周期。

组件的生命周期如下。

  1. 实例化。

  2. 给成员变量赋值。一般通过@Autowired等注解实现。

  3. 觉醒(Aware),如果组件实现了Aware相关的接口,在这时组件就可以通过该接口的set方法设置一些数据。比如,如果组件实现了BeanNameAware接口,那么就可以调用setBeanName(String beanName)方法。

  4. BeanPostProcessor,提供了两个实现方法:postProcessBeforeInitializationpostProcessAfterInitialization,其作用范围为所有注册的组件。此时触发的是before方法。

  5. InitializingBean的afterPropertiesSet方法 ,或是自定义的init方法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @Override
    public void afterPropertiesSet() throws Exception {
    System.out.println("InitializingBean提供的init方法");
    }

    // 自定义init方法,方法名自己定义
    @PostConstruct
    public void init() {
    System.out.println("自定义init");
    }
  6. BeanPostProcessor的after方法。

  7. 使用组件。

  8. DisposableBean的destroy方法,或是自定义的destroy方法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @Override
    public void destroy() throws Exception {
    System.out.println("DisposableBean提供的destroy");
    }

    @PreDestroy
    public void customDestroy() {
    System.out.println("自定义destroy");
    }

AOP

AOP,面向切面编程,可以将一些与业务无关,但是所有业务都需要调用的逻辑封装起来(如日志,权限控制等),将其作为一种增强。AOP基于动态代理实现,但是相比动态代理,AOP可以更灵活的筛选需要增强的方法以及增强所要做的业务。

现在一般使用AspectJ框架实现AOP。

切入点Pointcut

使用execution指定需要增强的方法。

1
2
3
4
5
6
7
<aop:comfig>
<aop:pointcut id="mypointcut1" expression="execution(public void
com.cskaoyan.service.UserServiceImpl.sayHello(String))"/>
<aop:pointcut id="mypointcut2" expression="execution(* co*..say*(*))"/>
<aop:pointcut id="mypointcut3" expression="execution(*
co*.cskaoyan..UserServiceImpl.sayHello(..))"/>
</aop:config>

使用@annotation指定特殊的增强方法。

1
2
3
4
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface CountTime {
}
1
2
3
4
5
<aop:config>
<!--作用场景:组件中的方法,包含自定义@CountTime注解的方法生效-->
<aop:pointcut id="mypointcut"
expression="@annotation(com.cskaoyan.CountTime)"/>
</aop:config>

通知器Advisor

1
2
3
4
<!--advisor → 新的标签 aop标签 → 引入aop的schema约束-->
<aop:config>
<aop:advisor advice-ref="countTimeInterceptor" pointcut="execution(public void com.cskaoyan.service.UserServiceImpl.sayHello(String))"/>
</aop:config>

切面Aspect

切面是上面两种的结合,即在指定的切入点写入自定义的通知。切面有五个通知方法,分别是Before、After、Around、AfterReturning、AfterThrowing。Before和After表示在该方法的执行前后的通知;Around类似于InvocationHandler的invoke()和MethodInterceptor的invoke(),可以在执行proceed方法前后进行操作,Around也是使用的最多的通知;AfterReturning是在方法返回值后使用的通知,可以接收方法的返回值;AfterThrowing是在方法抛出异常后使用的通知,可以接收方法的异常。

五个通知的先后顺序是:Before->Around->After/AfterThrowing->AfterReturning

可以使用@Aspect注解指定一个组件为切面组件,且在类中的方法要使用@Before、@After、@Around、@AfterReturning、@AfterThrowing注解修饰方法。

@Aspect
@Component
public class CustomAspect {

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
@Aspect
@Component
public class CustomAspect {

//@Pointcut注解增加在方法上 → value属性写切入点表达式,方法名作为切入点的id
@Pointcut("execution(* com.cskaoyan.service..*(..))")
public void servicePointcut() {
}

// 切面组件中的方法,作为对应的通知方法 → 在对应的时间点下会执行到对应的方法
// 通知注解的value属性:可以直接写切入点表达式;也可以引用切入点方法
@Before("servicePointcut()")
public void beforex() {
System.out.println("before方法");
}

@After("servicePointcut()")
public void after() {
System.out.println("after通知方法");
}

// Around通知 → 最强通知 :返回值为Object,要有委托类方法的执行
// 类似于InvocationHandler的invoke,类似于MethodInterceptor的invoke
// 执行委托类的方法:ProceedingJoinPoint proceed方法
@Around("servicePointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
System.out.println("开始时间:" + start);

Object proceed = joinPoint.proceed(); // 执行委托类的方法

long end = System.currentTimeMillis();
System.out.println("方法执行时间:" + (end - start));
return proceed; // 就是作为代理对象执行方法的返回值
}

// afterReturning通知方法的形参,采用Object类型,接收委托类方法的返回值
@AfterReturning(value = "servicePointcut()",returning = "result")
public void afterReturning(Object result) {
System.out.println("AfterReturning接收到的结果:" + result);
}

// public void afterThrowing(Exception exception) {
@AfterThrowing(value = "servicePointcut()",throwing = "exception")
public void afterThrowing(Throwable exception) {
System.out.println("afterThrowing通知:" + exception.getMessage());
}
}

连接点JoinPoint

在Before和Around通知执行过程中可以获得JoinPoint,通过它拿到一些信息,这意味着在通知中可以有更多操作空间。

joinPoint可以直接写在通知方法的形参中,通过调用其方法获取信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Before("servicePointcut()")
public void beforex(JoinPoint joinPoint) {
// 代理类
Object proxy = joinPoint.getThis();
// 目标类、委托类对象
Object target = joinPoint.getTarget();
// 参数
Object[] args = joinPoint.getArgs();
// 方法
Signature signature = joinPoint.getSignature();
String name = signature.getName();
System.out.println("正在执行的方法:" + name);

System.out.println("before方法");
}

Spring事务

Spring事务有三种事务传播行为,分别是REQUIRED、REQUIRES_NEW、NESTED。

REQUIRED:Spring默认。如果外围的方法不包含事务,那么被修饰的内部方法就添加一个新的事务;如果外围方法包含事务,则内部方法加入该事务,要么一起提交要么一起回滚。

REQUIRES_NEW:如果外围的方法不包含事务,那么内部方法就添加一个新的事务;如果外围方法包含事务,内部方法也添加一个事务,且该事务与外围的事务独立。

NESTED:如果外围的方法不包含事务,那么内部方法就添加一个新的事务;如果外围方法包含事务,内部方法也添加一个事务,且该事务与外围的事务成嵌套关系。

Spring事务的开启方式有:

  1. PlatFormTransactionManager,平台事务管理器

    1
    2
    3
    4
    5
    6
    7
    8
    public interface PlatformTransactionManager extends TransactionManager {
    // 根据事务的定义,获得事务的状态 → 开启事务
    TransactionStatus getTransaction(@Nullable TransactionDefinition var1) throws TransactionException;
    // 提交 → 参数是TransactionStatus
    void commit(TransactionStatus var1) throws TransactionException;
    // 回滚 → 参数是TransactionStatus
    void rollback(TransactionStatus var1) throws TransactionException;
    }
  2. TransactionTemplate

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    public class TransferTransactionCallBack implements TransactionCallback {

    AccountMapper accountMapper;
    Integer fromId;
    Integer destId;
    Integer fromMoney;
    Integer destMoney;

    public TransferTransactionCallBack(AccountMapper accountMapper, Integer fromId, Integer destId, Integer fromMoney, Integer destMoney) {
    this.accountMapper = accountMapper;
    this.fromId = fromId;
    this.destId = destId;
    this.fromMoney = fromMoney;
    this.destMoney = destMoney;
    }

    @Override
    public Object doInTransaction(TransactionStatus transactionStatus) {
    // 更新money
    int update1 = accountMapper.update(fromId, fromMoney);
    int i = 1 / 0;
    int update2 = accountMapper.update(destId, destMoney);
    return null;
    }
    }
  3. 在配置类使用@EnableTransactionManagement注解开启事务,在需要事务的类或方法中使用注解@Transactional。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    @Transactional
    @Override
    public void transfer(int fromId, int destId, Integer money) {
    // 查询现有的money是多少
    Integer fromMoney = accountMapper.select(fromId);
    Integer destMoney = accountMapper.select(destId);

    // 计算转账后的money是多少
    fromMoney -= money;
    destMoney += money;

    // 更新money
    int update1 = accountMapper.update(fromId, fromMoney);
    int i = 1 / 0;
    int update2 = accountMapper.update(destId, destMoney);

    }

SpringMVC

传统的Servlet映射过程繁琐,需要我们自己处理url和Servlet的映射关系,并根据url判断、分发接口至不同的方法中,且请求参数、响应结果都要自己手动拆封json。在SpringMVC中,则只由一个Servlet:DispatcherServlet来解决上述问题。

在SpringMVC中的流程如下。

  1. 客户端(浏览器)发送请求,直接请求到 DispatcherServlet
  2. DispatcherServlet 根据请求信息调用 HandlerMapping,解析请求对应的 Handler
  3. 解析到对应的 Handler(也就是我们平常说的 Controller 控制器)后,开始由 HandlerAdapter 适配器处理。
  4. HandlerAdapter 会根据 Handler来调用真正的处理器开处理请求,并处理相应的业务逻辑。
  5. 处理器处理完业务后,会返回一个 ModelAndView 对象,Model 是返回的数据对象,View 是个逻辑上的 View
  6. ViewResolver 会根据逻辑 View 查找实际的 View
  7. DispaterServlet 把返回的 Model 传给 View(视图渲染)。
  8. View 返回给请求者(浏览器)

在SpringMVC中,可以完全使用JavaConfig代替配置文件,主要有:

  1. AACDSI(AbstractAnnotationConfigDispatcherServletInitializer)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    public class ApplicationInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

    // 加载Spring的配置文件application.xml → 配置类
    @Override
    protected Class<?>[] getRootConfigClasses() {
    return new Class[]{SpringConfiguration.class};
    }

    // 加载SpringMVC的配置文件application-mvc.xml → 配置类
    @Override
    protected Class<?>[] getServletConfigClasses() {
    return new Class[]{MvcConfiguration.class};
    }

    // DispatcherServlet的映射范围
    @Override
    protected String[] getServletMappings() {
    return new String[]{"/"};
    }
    }
  2. Spring配置类,用@Configuration修饰

    1
    2
    3
    4
    5
    @ComponentScan(value = "com.cskaoyan",
    excludeFilters = @ComponentScan.Filter({Controller.class, EnableWebMvc.class})) // mvc配置类也在扫描包范围
    @Configuration
    public class SpringConfiguration {
    }
  3. SpringMVC配置类,用@EnableWebMvc修饰

    1
    2
    3
    4
    @ComponentScan("com.cskaoyan.controller")
    @EnableWebMvc
    public class MvcConfiguration implements WebMvcConfigurer {
    }

@RequestMapping

用于url的路径映射,可以写在类或方法上。

同一个RequestMapping可以映射多个路径,通过数组表示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RequestMapping({"hello","hello2","hello3"})
@ResponseBody
public String hello() {
return "hello world";
}

/**
* 也可以使用通配符 *
*/
@RequestMapping({"goodbye/*", "goodbye*"})
@ResponseBody
public String goodbye() {
return "byebye";
}

如果请求url包含共同的前缀,可以提取至修饰类的注解中,这称为窄化请求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Controller
@RequestMapping("user")
public class UserController {

//@RequestMapping("user/login")
@RequestMapping("login")
@ResponseBody
public String login() {
return "login";
}
//@RequestMapping("user/register")
@RequestMapping("register")
@ResponseBody
public String register() {
return "register";
}
//@RequestMapping("user/modify")
@RequestMapping("modify")
@ResponseBody
public String modify() {
return "modify";
}
//@RequestMapping("user/remove")
@RequestMapping("remove")
@ResponseBody
public String remove() {
return "remove";
}
}

@RequestMapping还有多种属性:

  • method:用于限定请求方法。如GET、POST。

    1
    2
    3
    4
    5
    @RequestMapping(value = "get",method = RequestMethod.GET)
    @ResponseBody
    public String methodGet() {
    return "Method GET";
    }
  • params:用于限定参数。

    1
    2
    3
    4
    5
    @RequestMapping(value = "login",params = {"username","password"}) // 既要携带username又要携带password
    @ResponseBody
    public String login() {
    return "ok";
    }
  • headers:用于限定请求头。

    1
    2
    3
    4
    5
    @RequestMapping(value = "limit", headers = {"abc", "def"})//既要携带abc、又要携带def这两个请求头
    @ResponseBody
    public String headerLimit() {
    return "header limit";
    }
  • consumes:用于限定Content-Type的值。

  • produces:用于限定Accept的值

@ResponseBody

在Controller上添加注解@ResponseBody,即可将返回值自动包装为json。

1
2
3
4
5
6
7
8
9
@Controller
public class JsonController {

@RequestMapping("json")
@ResponseBody
public User json() {
return new User("123","456");
}
}

可以使用@RestController来代替@Controller和 @ResponseBody。

请求参数的接收

如果是普通的get请求,可以使用方法形参来接收参数,但要求形参名与参数名必须一致。类型支持:

  • 字符串String
  • 基本类型以及对应包装类
  • 数组
  • Date(需要使用@DateTimeFormat来标准化日期格式)
  • 文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@RequestMapping("register5")
public BaseRespVo register5(String username, String password, Integer age,
String[] hobbies, Integer[] ids,
@DateTimeFormat(pattern = "yyyy-MM-dd") Date birthday) {
return BaseRespVo.ok();
}

@RequestMapping("file")
public BaseRespVo uploadFile(MultipartFile file) throws IOException {
// 获得上传的文件的信息
String name = file.getName(); // 请求参数名 → file
String originalFilename = file.getOriginalFilename(); //原始文件名
long size = file.getSize(); // 文件的大小
String contentType = file.getContentType(); // 正文类型 → 文件类型
// MultipartFile.transferTo
File saveFile = new File("D:\\WorkSpace\\j40_workspace", originalFilename);
//File saveFile = new File("D:\\WorkSpace\\j40_workspace\\dlrb.jpg");
file.transferTo(saveFile);
return BaseRespVo.ok();
}

除了形参,还可以使用自定义的引用类型对象,如果使用引用类型的对象时,Servlet会去创建一个新的实例。看你这个类中有哪些成员变量,如果成员变量名和请求参数名相同,就会使用成员变量来接收请求参数,使用set方法来进行封装。所以要求请求参数名与引用类中的成员变量名一致。

1
2
3
4
5
6
7
8
9
10
@Data
public class User {
String username;
String password;
Integer age;
String[] hobbies;
Integer[] ids;
@DateTimeFormat(pattern = "yyyy-MM-dd")
Date birthday;
}
1
2
3
4
@RequestMapping("register6")
public BaseRespVo register6(User user) {
return BaseRespVo.ok();
}

如果是post请求,那么发送的请求参数一般是json,需要对json转换成对应的实例。此时需要增加@RequestBody注解来修饰json对应的bean类。

1
2
3
4
@RequestMapping("user/login")
public BaseRespVo login(@RequestBody JsonUser user) {
return BaseRespVo.ok(user);
}

如果需要获得Cookie,则不能直接获得,需要通过Request获得。

1
2
3
4
5
6
7
8
9
10
// 把所有Cookie都遍历打印一下
@RequestMapping("cookies")
public BaseRespVo cookies(HttpServletRequest request) {
Cookie[] cookies = request.getCookies();
for (Cookie cookie : cookies) {
System.out.println(cookie.getName() + " --> " + cookie.getValue());
}

return BaseRespVo.ok();
}

而session可以直接获得。

1
2
3
4
5
@RequestMapping("session2")
public BaseRespVo session2(HttpSession session) {
Object username = session.getAttribute("username");
return BaseRespVo.ok(username);
}

@PathVariable

获取请求url中指定占位符的值,并将值赋给对应的形参。

1
2
3
4
@RequestMapping("note/{id}")
public BaseRespVo note(@PathVariable("id") Integer id) {
return BaseRespVo.ok();
}

Converter

Converter,类型转换器,使用主要是在请求参数封装的过程中,也就是Handler方法的形参。在形参并不是String的情况下会用到Converter。

一般情况下需要自己定义Converter。如从String转为User。

1
2
3
4
5
6
7
8
9
10
public class String2UserConverter implements Converter<String, User> {
@Override
public User convert(String s) {
// 里面的转换业务需要你自己定义
User user = new User();
user.setUsername(s);
user.setPassword(s);
return user;
}
}
1
2
3
4
5
// 提供自定义的转换器
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new String2UserConverter());
}

ResourceHandler

因为在SpringMVC中,DispatcherServlet的映射范围是/,即除了jsp外的所有请求,之前JavaEE的访问静态资源的方式就无法使用了,所以需要使用ResourceHandler来进行静态资源映射。

1
2
3
4
5
6
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/pic/**").addResourceLocations("classpath:/");
registry.addResourceHandler("/jpg/**").addResourceLocations("/");
registry.addResourceHandler("/png/**").addResourceLocations("file:D:\\spring/");
}

HandlerInterceptor

在Handler前执行,起到了拦截的功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
public interface HandlerInterceptor {
// 在Handler方法执行之前
// 返回值为boolean,如果为true则继续流程,如果false,则中断流程
default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
return true;
}
// 在Handler方法执行之后
default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {
}
// 在整个流程完成之后
default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
}
}

异常处理

有两种异常处理方式:统一的全局异常处理 HandlerExceptionResolver和自定义异常处理 ExceptionHandler。

HandlerExceptionResolver是一个接口,接口里提供了resolveException方法,重写该方法来处理全局异常。

1
2
3
4
5
6
7
8
9
10
11
12
@Component
public class CustomHandlerExceptionResolver implements HandlerExceptionResolver {
@Override
public ModelAndView resolveException(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) {
// 通过它也可以做个性化的异常处理,如果要做,需要你自己来做个性化的东西
if (e instanceof ArithmeticException) {
//做个性化的异常处理
System.out.println(e.getMessage());
}
return new ModelAndView("/exception.jsp");
}
}

ExceptionHandler是一个注解,value是对应的异常,旨在用对应的方法处理对应的异常。一般放在ontrollerAdvice组件中的方法上。

1
2
3
4
5
6
7
8
9
10
11
@ControllerAdvice
public class CustomExceptionControllerAdvice {

// 在形参中可以直接接收你抛出的异常,你映射的是什么异常,就可以以什么类型的形参来接收
@ExceptionHandler(ArithmeticException.class)
@ResponseBody
public BaseRespVo method1(ArithmeticException exception) {
return BaseRespVo.fail(exception.getMessage());
}

}

SpringBoot

SpringBoot本质上就是一个Spring框架,但是它简化了Spring的配置。在没有给具体配置前,SpringBoot会使用默认的配置,只有给到具体的配置,才采用具体的配置。

启动类:

1
2
3
4
5
6
7
8
9
// 该注解就是SpringBoot应用启动类上的注解 → 会配置扫描包目录就是该类所处的包目录
@SpringBootApplication
public class Demo1FirstSbApplication {
// 启动类中一定会包含main方法 → 就是启动SpringBoot应用程序的入口
public static void main(String[] args) {
SpringApplication.run(Demo1FirstSbApplication.class, args);
}

}

SpringBoot对web的支持:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# tomcat配置
server:
port: 8083 #Tomcat的端口号配置
servlet:
context-path: /demo1 # 应用程序的上下文配置(应用名)

# 静态资源映射
spring:
mvc:
static-path-pattern: /pic/**
# spring.resources 或 spring.web.resources
resources:
static-locations: file:d:/spring/

# Converter
# Filter

SpringBoot对Mybatis的支持:

1
2
3
4
5
6
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/cskaoyan_db?useUnicode=true&characterEncoding=utf-8
username: root
password: 123456
1
2
3
4
5
6
7
8
9
10
@SpringBootApplication
// 需要添加@MapperScan来扫描mapper
@MapperScan("com.cskaoyan.mapper")
public class Demo1FirstSbApplication {

public static void main(String[] args) {
SpringApplication.run(Demo1FirstSbApplication.class, args);
}

}

逆向工程

逆向工程可以根据数据库中的表自动生成接口、方法、映射文件,只需要操作对应的Example类就可以操作数据库。

接口方法示例。

1
2
3
4
5
6
//升序还是降序:字段+空格+asc(desc)
protected String orderByClause;
//去除重复:true是选择不重复记录,false,反之
protected boolean distinct;
//自定义查询条件
protected List<Criteria> oredCriteria;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Test
public void testFindUserByName(){

//通过criteria构造查询条件
UserExample userExample = new UserExample();
userExample.setOrderByClause("username asc"); //asc升序,desc降序排列
userExample.setDistinct(false); //去除重复,true是选择不重复记录,false反之
UserExample.Criteria criteria = userExample.createCriteria(); //构造自定义查询条件
criteria.andUsernameEqualTo("张三");

//自定义查询条件可能返回多条记录,使用List接收
List<User> users = userMapper.selectByExample(userExample);

System.out.println(users);
}

逆向工程生成的内容会有一定问题,如果表中的字段存在sql语句中的关键字,需要自己手动在mapper.xml文件中添加转义字符。

TypeHandler

TypeHandler是Mybatis输入输出映射过程中的类型处理器,用于处理jdbcType和JavaType间的转换。

主要方式是实现TypeHandler接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// 类型映射配置
@MappedTypes(Integer[].class)
@MappedJdbcTypes(JdbcType.VARCHAR)
public class IntegerArrayTypeHandler implements TypeHandler<Integer[]> {

ObjectMapper objectMapper = new ObjectMapper();

// 输入映射过程
// insert into market_user (id,username,password,role_ids) values (?,?,?,?)
@Override
public void setParameter(PreparedStatement preparedStatement, int index, Integer[] integers, JdbcType jdbcType) throws SQLException {
String value = null;
try {
value = objectMapper.writeValueAsString(integers);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
// 为第几个占位符提供的值是什么
preparedStatement.setString(index,value);
}

// 输出映射过程
@Override
public Integer[] getResult(ResultSet resultSet, String columName) throws SQLException {
// 获得结果
String result = resultSet.getString(columName);
return transfer(result);
}

@Override
public Integer[] getResult(ResultSet resultSet, int index) throws SQLException {
// 获得结果
String result = resultSet.getString(index);
return transfer(result);
}

@Override
public Integer[] getResult(CallableStatement callableStatement, int i) throws SQLException {
// 获得结果
String result = callableStatement.getString(i);
return transfer(result);
}

private Integer[] transfer(String result) {
Integer[] integers = new Integer[0];
// 使用jackson将字符串转换Integer[]
try {
integers = objectMapper.readValue(result, Integer[].class);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
return integers;
}
}

配置自定义的TypeHandler

1
2
mybatis:
type-handlers-package: com.cskaoyan.typehandler

Shiro框架

Shiro安全框架,Apache Shiro是一个开源安全框架,提供身份认证、授权、密码学和会话管理。Shiro框架直观、易用,同时也能提供健壮的安全性。

Shiro在SpringMVC应用程序中以Filter的形式存在。

Shiro的一些核心术语如下。

  • Authentication,认证是身份验证的过程 -您正在尝试证明用户是他们所说的人。为此,用户需要提供系统理解和信任的某种身份证明。认证其实就是我们通常所说的登录。
  • Authorization,授权或访问控制是指定对资源的访问权限的功能。换句话说,谁可以访问什么。比如是否允许用户编辑此数据,这些都是决定用户有权访问的内容的决定。我们当前主要针对的是URL级别的权限访问控制。
  • Subject,主体,在Shiro中所做的几乎所有操作都基于当前正在执行的用户,也就是基本上Shiro的操作都是使用Subject操作的,Subject指的就是当前操作的用户。在代码中的任何位置都可以轻松获得Subject,通过Subject可以方便的操作Shiro。比如我们可以使用Subject提供的方法来执行认证、判断是否认证等操作。
  • Principals,主体鉴定后的参数,也就是认证后的用户信息,可以是姓名、用户id、用户对象等形式。
  • Credentials,用来验证身份的秘密数据,通常指密码,生物数据比如指纹、面部、瞳孔等。
  • Realms,域或领域,安全的特殊数据存储对象(DAO),Shiro中的Realm主要是让你能够获得对应的认证信息和授权信息。
  • Token,令牌,Shiro中的Token是作为登录操作的参数。

Shiro的核心组件如下:

  • SecurityManager,安全管理器
  • Authenticator,认证器
  • SessionManager,会话管理器
  • Realm,域
  • CacheManager,缓存管理器
  • Cryptography,密码学

Shiro提供的Filter类型如下。

Filter名称 Filter类 说明
anon org.apache.shiro.web.filter.authc.AnonymousFilter 匿名Filter,作用范围内的请求不需要认证和授权
authc org.apache.shiro.web.filter.authc.FormAuthenticationFilter 认证Filter,作用范围内的请求需要完成认证
perms org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter 权限Filter,作用范围内的请求需要完成认证和授权

我们可以设置不同类型的Filter分别映射一些不同的URL范围,当请求发送到应用程序时,根据请求URL分别判断使用哪一些Filter,在Filter中决定是否继续访问流程。在SpringBoot中,主要在配置文件中配置。

Realm

Shiro中的Realm主要是让你能够获得对应的认证信息和授权信息。

一般通过自定义Realm来使用,需要继承一个抽象类AuthorizingRealm,并实现两个抽象方法:

  • doGetAuthenticationInfo :获得认证信息,即根据token中的用户名查询该用户在系统中的Credentials,并且构造AuthenInfo。
  • doGetAuthorizationInfo :获得授权信息,即根据Principal(放入AuthenInfo中的第一个参数)查询该用户在系统中的权限信息。

在这两个方法中去写获得用户认证信息和授权信息的业务代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
@Component
public class CustomRealm extends AuthorizingRealm {
//通常把doGetAuthenticationInfo方法放在上面

//该方法的形参 → 来源于subject的login方法
// 传入该值,为了根据用户名查询到该用户在系统中的密码(数据库中维护) → 来构造认证信息
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
String username = token.getUsername();

//根据username查询数据库中对应password
String password = selectPasswordByUsername(username);

// principal信息 → 当前放的是什么信息,后续取出的就是什么信息
// 密码 → 该密码会和Token中的password做比较
// realm名称 → 没啥用
return new SimpleAuthenticationInfo(username,password,getName());
}

@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
// 根据用户信息拿到所有的权限(数据库)
// doGetAuthenticationInfo方法返回值的第一个参数就是用户信息
// 在第21行代码放入的是字符串类型的Principal信息,取出的时候就可以以字符串格式取出
String primaryPrincipal = (String) principalCollection.getPrimaryPrincipal();
List<String> permissions = getPermissionsByUsername(primaryPrincipal);

SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
simpleAuthorizationInfo.addStringPermissions(permissions);
return simpleAuthorizationInfo;
}

private String selectPasswordByUsername(String username) {
// 应该通过MyBatis根据用户名查询获得结果

return "123456";
}

private List<String> getPermissionsByUsername(String username) {
// 应该通过MyBatis根据用户名查询获得结果
return Arrays.asList("admin:user:list", "admin:admin:list");
}

}

认证

在对应请求中获得subject,并执行subject的login方法。

  • 因为登录请求不需要权限,需要将其权限设为anon
  • subject对象可以在所有组件中获得,方式为Subject subject = SecurityUtils.getSubject()
  • 在绝大多数场景下,login方法的参数都为AuthenticationToken接口,可以直接使用其实现类UsernamePasswordToken: subject.login(new UsernamePasswordToken(username,password))
  • 如果需要获得sessionId,可以使用subject获得: subject.getSession().getId()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@PostMapping("login")
public BaseRespVo<LoginUserData> login(@RequestBody Map map) {
String username = (String)map.get("username");
String password = (String)map.get("password");

// 整合Shiro
// 获得操作的主体
Subject subject = SecurityUtils.getSubject();
// login方法传入的参数AuthenticationToken → 认证的令牌
// subject执行login → 认证器执行认证方法 → realm.doGetAuthenticationInfo
// AuthenticationToken → UsernamePasswordToken → 直接封装了username和password
// username和password通过Handler方法的形参传入
subject.login(new UsernamePasswordToken(username,password));

if (subject.isAuthenticated()) {
System.out.println("认证成功");
}

LoginUserData loginUserData = new LoginUserData();
AdminInfoBean adminInfo = new AdminInfoBean();
adminInfo.setAvatar("https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif");
adminInfo.setNickName("admin123");
loginUserData.setAdminInfo(adminInfo);
// 携带SessionId信息
loginUserData.setToken((String) subject.getSession().getId());
return BaseRespVo.ok(loginUserData);
}
  • 认证后可以通过subject.getPrincipals().getPrimaryPrincipal()来获取用户在数据库中的其他信息。该方法的返回值为Object,可以直接转换为对应的用户信息PO类。
  • 可以通过subject.logout()登出用户。

HibernateValidation

在需要对一些参数进行校验时,可以在Handler方法的形参上增加注解@Valid或@Validated,或在引用类型中的成员变量上增加校验功能的注解。

常见注解如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
常见的注解 (Bean Validation 中内置的 constraint)     
@Null 被注释的元素必须为 null
@NotNull 被注释的元素必须不为 null
@Size(max=, min=) 被注释的元素的大小必须在指定的范围内
@AssertTrue 被注释的元素必须为 true
@AssertFalse 被注释的元素必须为 false
@Min(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@Max(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@DecimalMin(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@DecimalMax(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@Digits (integer, fraction) 被注释的元素必须是一个数字,其值必须在可接受的范围内
@Past 被注释的元素必须是一个过去的日期 Date
@Future 被注释的元素必须是一个将来的日期
@Pattern(regex=,flag=) 被注释的元素必须符合指定的正则表达式
Hibernate Validator 附加的 constraint
@NotBlank(message =) 验证字符串非null,且长度必须大于0
@Email 被注释的元素必须是电子邮箱地址
@Length(min=,max=) 被注释的字符串的大小必须在指定的范围内
@NotEmpty 被注释的字符串的必须非空
@Range(min=,max=,message=) 被注释的元素必须在合适的范围内
本文作者:Ykuri98
本文链接:https://ykuri98.github.io/2022/07/04/%E5%AD%A6%E4%B9%A0%E6%80%BB%E7%BB%93%EF%BC%882022-06-15-2022-06-24%EF%BC%89/
版权声明:本文采用 CC BY-NC-SA 3.0 CN 协议进行许可
×