几个注解
@ConditionalOnMissingBean
只有特定名称或者类型的Bean(通过@ConditionalOnMissingBean修饰)不存在于BeanFactory中时才创建某个Bean
// 只有BeanFactory中没有 imageValidateCodeGenerator这个Bean时才创建
@Bean
@ConditionalOnMissingBean(name = "imageValidateCodeGenerator")
public ValidateCodeGenerator imageValidateCodeGenerator() {
ImageCodeGenerator codeGenerator = new ImageCodeGenerator();
codeGenerator.setSecurityProperties(securityProperties);
return codeGenerator;
}
@ConditionalOnBean
和@ConditionalOnMissingBean对应,当BeanFactory中存在某个时才创建
@ConditionalOnClass
类加载器中存在对应的类就执行
@ConditionalOnMissingClass
与@ConditionalOnClass作用一样,条件相反,类加载器中不存在对应的类才执行
有一种东西叫依赖查找,不知道听过没有
@Autowired
private Map<String,DemoService> demoServiceMap;
Spring会将DemoService类型的Bean的名字作为key,对象作为value封装进入Map。同理,还可以使用List的方式
MockMvc
为什么要使用测试?可以避免启动内置的web容器,速度会快很多。
添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
两个关键注解
// 表示以SpringRunner来执行测试用例
@RunWith(SpringRunner.class)
// 声明当前类为一个测试用例
@SpringBootTest
public class UserControllerTest {
}
WireMock
可以认为WireMock是一个单独的服务器,用来模拟一些数据,可以通过代码控制。

下载WireMock
启动WireMock
java -jar wiremock-standalone-2.18.0.jar
启动之后就可以直接给前端或者APP使用了,让它单独在服务器上运行就可以了。至于需要什么样的接口,则是在我们的应用中通过代码来控制
添加依赖
<dependency>
<groupId>com.github.tomakehurst</groupId>
<artifactId>wiremock</artifactId>
</dependency>
为WireMock定义接口
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
import static com.github.tomakehurst.wiremock.client.WireMock.configureFor;
import static com.github.tomakehurst.wiremock.client.WireMock.get;
import static com.github.tomakehurst.wiremock.client.WireMock.removeAllMappings;
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;
import java.io.IOException;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.StringUtils;
import org.springframework.core.io.ClassPathResource;
public class MockServer {
/**
* @param args
* @throws IOException
*/
public static void main(String[] args) throws IOException {
// 8062是指刚刚启动的WireMock的端口
configureFor(8062);
// 清空之前的缓存,相当于是每次启动的时候都刷新接口
removeAllMappings();
mock("/order/1", "01");
mock("/order/2", "02");
}
private static void mock(String url, String file) throws IOException {
ClassPathResource resource = new ClassPathResource("mock/response/" + file + ".txt");
String content = StringUtils.join(FileUtils.readLines(resource.getFile(), "UTF-8").toArray(), "\n");
stubFor(get(urlPathEqualTo(url)).willReturn(aResponse().withBody(content).withStatus(200)));
}
}
Swagger2
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.2.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.2.2</version>
</dependency>
添加一个配置类
@Configuration
@EnableSwagger2
public class Swagger {
@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("com.hand.hap.cloud.hdip"))
.paths(PathSelectors.any())
.build();
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("ServiceName Api")
.description("ServiceName Api Description")
.termsOfServiceUrl("localhost:8080")
.contact("spilledyear")
.version("1.0")
.build();
}
}
@JsonView
这个用于控制返回dto中的哪些字段
public class User {
public interface UserSimpleView {}
public interface UserDetailView extends UserSimpleView {}
private String id;
private String username;
private String password;
@JsonView(UserSimpleView.class)
public String getUsername() {
return username;
}
@JsonView(UserDetailView.class)
public String getPassword() {
return password;
}
}
在上面这段代码种,定义了两个JsonView:UserDetailView 和 UserSimpleView,其中UserSimpleView 继承了 UserSimpleView, 说明UserSimpleView返回的json中除了包含自己定义的password字段,还可以返回username字段
定义好了之后,接下来就可以直接在Controller中使用了, 以下返回的json串中将仅包含name属性
@GetMapping
@JsonView(User.UserSimpleView.class)
@ApiOperation(value = "用户查询服务")
public List<User> query(UserQueryCondition condition, @PageableDefault(page = 2, size = 17, sort = "username,asc") Pageable pageable) {
List<User> users = new ArrayList<>();
users.add(new User());
users.add(new User());
users.add(new User());
return users;
}
用起来感觉有点麻烦,看情况使用吧。
Hibernate Validator
用于数据校验!比如在一些字段上添加一些注解,然后通过@Valid 和 BindingResult 使用
public class User {
@NotBlank(message = "密码不能为空")
private String password;
}
@PutMapping("/{id:\\d+}")
public User update(@Valid @RequestBody User user, BindingResult errors) {
user.setId("1");
return user;
}
如果封装的那些注解不能满足需求,可以自定义注解
@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
// validatedBy = MyConstraintValidator.class 表示你的校验逻辑在MyConstraintValidator类中
@Constraint(validatedBy = MyConstraintValidator.class)
public @interface MyConstraint {
String message();
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
}
自定义校验逻辑
public class MyConstraintValidator implements ConstraintValidator<MyConstraint, Object> {
@Autowired
private HelloService helloService;
// 初始化时候的逻辑
@Override
public void initialize(MyConstraint constraintAnnotation) {
System.out.println("my validator init");
}
// 校验逻辑
@Override
public boolean isValid(Object value, ConstraintValidatorContext context) {
helloService.greeting("tom");
System.out.println(value);
return true;
}
}
使用的时候,只需要添加到字段上面就可以了
public class User {
@MyConstraint(message = "这是一个测试")
@ApiModelProperty(value = "用户名")
private String username;
}
异常处理
浏览器发请求返回html;非浏览器发请求返回Json
@Controller
@RequestMapping({"${server.error.path:${error.path:/error}}"})
public class BasicErrorController extends AbstractErrorController {
// // 返回html
@RequestMapping(produces = {"text/html"})
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response){
HttpStatus status = this.getStatus(request);
Map<String, Object> model = Collections.unmodifiableMap(this.getErrorAttributes(request, this.isIncludeStackTrace(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
ModelAndView modelAndView = this.resolveErrorView(request, response, status, model);
return modelAndView == null ? new ModelAndView("error", model) : modelAndView;
}
// 返回json
@RequestMapping
@ResponseBody
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
Map<String, Object> body = this.getErrorAttributes(request, this.isIncludeStackTrace(request, MediaType.ALL));
HttpStatus status = this.getStatus(request);
return new ResponseEntity(body, status);
}
}
修改Springboot中默认异常html界面
注意目录结构,在这里面弄进行覆盖

修改Springboot中默认异常json
定义一个异常
public class UserNotExistException extends RuntimeException {
private static final long serialVersionUID = -6112780192479692859L;
private String id;
public UserNotExistException(String id) {
super("user not exist");
this.id = id;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
}
定义一个异常处理通知
@ControllerAdvice
public class ControllerExceptionHandler {
// 这里面定义UserNotExistException异常返回的内特容
@ExceptionHandler(UserNotExistException.class)
@ResponseBody
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public Map<String, Object> handleUserNotExistException(UserNotExistException ex) {
Map<String, Object> result = new HashMap<>();
result.put("id", ex.getId());
result.put("message", ex.getMessage());
return result;
}
}
拦截方式
可以通过 Filter、Interceptor、Aspect 进行拦截
过滤器Filter
让一个Filter在 Springboot中生效有两种
- 通过@Component注解
@Component
public class TimeFilter implements Filter {
@Override
public void destroy() {
System.out.println("time filter destroy");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
System.out.println("time filter start");
long start = new Date().getTime();
chain.doFilter(request, response);
System.out.println("time filter 耗时:"+ (new Date().getTime() - start));
System.out.println("time filter finish");
}
@Override
public void init(FilterConfig arg0) throws ServletException {
System.out.println("time filter init");
}
}
- 通过配置类。比如你想让第三方框架中的某个Filter生效,这时候无法声明@Component注解
public class TimeFilter implements Filter {
@Override
public void destroy() {
System.out.println("time filter destroy");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
System.out.println("time filter start");
long start = new Date().getTime();
chain.doFilter(request, response);
System.out.println("time filter 耗时:"+ (new Date().getTime() - start));
System.out.println("time filter finish");
}
@Override
public void init(FilterConfig arg0) throws ServletException {
System.out.println("time filter init");
}
}
@Configuration
public class WebConfig{
@Bean
public FilterRegistrationBean timeFilter() {
FilterRegistrationBean registrationBean = new FilterRegistrationBean();
TimeFilter timeFilter = new TimeFilter();
registrationBean.setFilter(timeFilter);
List<String> urls = new ArrayList<>();
urls.add("/*");
registrationBean.setUrlPatterns(urls);
return registrationBean;
}
}
拦截器Interceptor
先定义一个Interceptor,注意,直接这样是不能生效的,还需要配置
@Component
public class TimeInterceptor implements HandlerInterceptor {
// 执行目标方法前,该方法的返回值决定接下来的代码是否执行,比如 Controller中的方法、postHandle
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
System.out.println("preHandle");
System.out.println(((HandlerMethod)handler).getBean().getClass().getName());
System.out.println(((HandlerMethod)handler).getMethod().getName());
request.setAttribute("startTime", new Date().getTime());
return true;
}
// 抛异常不执行, Controller中的方法刚执行完就执行这个方法
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
System.out.println("postHandle");
Long start = (Long) request.getAttribute("startTime");
System.out.println("time interceptor 耗时:"+ (new Date().getTime() - start));
}
// 必定会执行
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
throws Exception {
System.out.println("afterCompletion");
Long start = (Long) request.getAttribute("startTime");
System.out.println("time interceptor 耗时:"+ (new Date().getTime() - start));
System.out.println("ex is "+ex);
}
}
配置让该Interceptor生效
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
@SuppressWarnings("unused")
@Autowired
private TimeInterceptor timeInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(timeInterceptor);
}
}
切面Aspect
这其实是属于SpringAOP的内容了。相对于前两个,这种方式可以在拦截的时候拿到目标方法中的参数值
添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
定义一个切面
@Aspect
@Component
public class TimeAspect {
@Around("execution(* com.imooc.web.controller.UserController.*(..))")
public Object handleControllerMethod(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("time aspect start");
Object[] args = pjp.getArgs();
for (Object arg : args) {
System.out.println("arg is "+arg);
}
long start = new Date().getTime();
// 执行目标方法
Object object = pjp.proceed();
System.out.println("time aspect 耗时:"+ (new Date().getTime() - start));
System.out.println("time aspect end");
return object;
}
}
执行顺序:Filter --> Interceptor --> Advice --> Controller
上传下载
Springboot处理文件上传下载,实际项目中文件上传可能仅仅是提交文件信息,而文件交由专用服务器处理
文件上传
测试代码
@Test
public void whenUploadSuccess() throws Exception {
String result = mockMvc.perform(fileUpload("/file")
.file(new MockMultipartFile("file", "test.txt", "multipart/form-data", "hello upload".getBytes("UTF-8"))))
.andExpect(status().isOk())
.andReturn().getResponse().getContentAsString();
System.out.println(result);
}
对应Controller逻辑
@PostMapping
public FileInfo upload(MultipartFile file) throws Exception {
String folder = "./";
System.out.println(file.getName());
System.out.println(file.getOriginalFilename());
System.out.println(file.getSize());
File localFile = new File(folder, new Date().getTime() + ".txt");
file.transferTo(localFile);
return new FileInfo(localFile.getAbsolutePath());
}
文件下载
@GetMapping("/{id}")
public void download(@PathVariable String id, HttpServletRequest request, HttpServletResponse response) throws Exception {
String folder = "./";
try (InputStream inputStream = new FileInputStream(new File(folder, id + ".txt"));
OutputStream outputStream = response.getOutputStream();) {
response.setContentType("application/x-download");
response.addHeader("Content-Disposition", "attachment;filename=test.txt");
IOUtils.copy(inputStream, outputStream);
outputStream.flush();
}
}
异步处理REST
异步请求在Springboot中的应用
Runable

@RequestMapping("/order")
public DeferredResult<String> order() throws Exception {
logger.info("主线程开始");
Callable<String> result = new Callable<String>() {
@Override
public String call() throws Exception {
logger.info("副线程开始");
Thread.sleep(1000);
logger.info("副线程返回");
return "success";
}
};
}
DeferredResult

DeferredResult用于两个线程间的交互:比如请求线程、返回线程
@Autowired
private MockQueue mockQueue;
@Autowired
private DeferredResultHolder deferredResultHolder;
private Logger logger = LoggerFactory.getLogger(getClass());
@RequestMapping("/order")
public DeferredResult<String> order() throws Exception {
logger.info("主线程开始");
// 生成随机订单号
String orderNumber = RandomStringUtils.randomNumeric(8);
// 模拟消息队列发送消息
mockQueue.setPlaceOrder(orderNumber);
DeferredResult<String> result = new DeferredResult<>();
deferredResultHolder.getMap().put(orderNumber, result);
return result;
}
@Component
public class DeferredResultHolder {
private Map<String, DeferredResult<String>> map = new HashMap<String, DeferredResult<String>>();
public Map<String, DeferredResult<String>> getMap() {
return map;
}
public void setMap(Map<String, DeferredResult<String>> map) {
this.map = map;
}
}
@Component
public class QueueListener implements ApplicationListener<ContextRefreshedEvent> {
@Autowired
private MockQueue mockQueue;
@Autowired
private DeferredResultHolder deferredResultHolder;
private Logger logger = LoggerFactory.getLogger(getClass());
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
new Thread(() -> {
while (true) {
if (StringUtils.isNotBlank(mockQueue.getCompleteOrder())) {
String orderNumber = mockQueue.getCompleteOrder();
logger.info("返回订单处理结果:"+orderNumber);
deferredResultHolder.getMap().get(orderNumber).setResult("place order success");
mockQueue.setCompleteOrder(null);
}else{
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
}
异步配置
主要是和异步有关的一些配置,比如异步情况下的拦截器配置
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
@Override
public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
configurer.registerCallableInterceptors(xxx);
......
}
}
Spring Security
返回html还是Json
非常非常常用的场景,后台写了一个接口,比如说登录成功之后,如果是在本系统,可能是直接返回一个界面;如果是前后端分离架构、或者是app应用,这时候需要返回一个json字符串,这就要求后台接口根据不同的清空返回不同的内容,如果是html请i去,就返回界面,如果不是html请求,就返回Json
@RestController
public class BrowserSecurityController {
private RequestCache requestCache = new HttpSessionRequestCache();
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
@Autowired
private SecurityProperties securityProperties;
@RequestMapping(SecurityConstants.DEFAULT_UNAUTHENTICATION_URL)
@ResponseStatus(code = HttpStatus.UNAUTHORIZED)
public SimpleResponse requireAuthentication(HttpServletRequest request, HttpServletResponse response)
throws IOException {
SavedRequest savedRequest = requestCache.getRequest(request, response);
if (savedRequest != null) {
String targetUrl = savedRequest.getRedirectUrl();
logger.info("引发跳转的请求是:" + targetUrl);
if (StringUtils.endsWithIgnoreCase(targetUrl, ".html")) {
redirectStrategy.sendRedirect(request, response, securityProperties.getBrowser().getLoginPage());
}
}
return new SimpleResponse("访问的服务需要身份认证,请引导用户到登录页");
}
}
// 用于读取配置文件imooc.security开头的属性,然后放到 BrowserProperties对象中
@ConfigurationProperties(prefix = "imooc.security")
public class SecurityProperties {
private BrowserProperties browser = new BrowserProperties();
public BrowserProperties getBrowser() {
return browser;
}
public void setBrowser(BrowserProperties browser) {
this.browser = browser;
}
}
// 如果一个配置类开启 配置文件的读取
@Configuration
@EnableConfigurationProperties(SecurityProperties.class)
public class SecurityCoreConfig {
}
验证码
在UsernamePasswordAuthenticateFilter 过滤器之前添加一个过滤器,即 验证码过滤器。大致思路,生成 验证码,存在session中,然后在过滤器中校验
// 前端关键代码,/code/image 即使对应Controller请求路径
<tr>
<td>图形验证码:</td>
<td>
<input type="text" name="imageCode">
<img src="/code/image?width=200">
</td>
</tr>
// 在配置类中开启 /code/image 访问
.authorizeRequests().antMatchers("/code/image");
// 编写过滤器
public class ValidateCodeFilter extends OncePerRequestFilter{
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
ValidateCodeType type = getValidateCodeType(request);
if (type != null) {
logger.info("校验请求(" + request.getRequestURI() + ")中的验证码,验证码类型" + type);
try {
validateCodeProcessorHolder.findValidateCodeProcessor(type)
.validate(new ServletWebRequest(request, response));
logger.info("验证码校验通过");
} catch (ValidateCodeException exception) {
authenticationFailureHandler.onAuthenticationFailure(request, response, exception);
return;
}
}
chain.doFilter(request, response);
}
}
// 将该过滤器添加到 UsernamePasswordAuthenticateFilter 前面
ValidateCodeFilter valicateCodeFilter = new ValidateCodeFilter();
http.addFilterBefore(valicateCodeFilter, UsernamePasswordAuthenticateFilter.class)
网友评论