升级SpringBoot 2.6与Wicket 9.8后CSV文件下载功能失效求助
问题:SpringBoot 2.1→2.6、Wicket 8.6→9.8升级后CSV下载功能失效
升级后其他功能正常,CSV文件能正常生成但无法触发下载,无报错信息,以下是相关代码及排查方向:
排查方向
- Wicket 9.x的Ajax行为变化:Wicket 9对Ajax请求处理有调整,原
AjaxFileDownload中字符串拼接的JS跳转逻辑可能因浏览器安全策略(如CSP、XSS防护)被拦截。建议改用安全的JS拼接方式:target.appendJavaScript(String.format("setTimeout(function(){window.location.href='%s';}, 100);", url)); - 资源请求处理器调度逻辑:Wicket 9中
scheduleRequestHandlerAfterCurrent的行为有变更,可尝试在onRequest中直接使用requestCycle.replaceAllRequestHandlers(handler)替代调度,同时检查Spring Security等过滤器是否拦截了回调请求。 - 回调URL有效性验证:在浏览器开发者工具中查看网络请求,确认
getCallbackUrl()生成的URL是否可访问、返回状态码是否为200,响应头是否包含Content-Disposition: attachment。 - 文件生命周期检查:临时注释
Files.remove(file)代码,确认是否因下载完成前文件被删除导致下载失败。 - SpringBoot请求拦截配置:SpringBoot 2.6调整了WebMvc配置,检查自定义过滤器或拦截器是否阻止了Wicket的回调请求,确保
/wicket/resource/...这类路径被放行。
相关代码
AjaxFileDownload类
import java.io.File; import org.apache.wicket.ajax.AjaxRequestTarget; import org.apache.wicket.behavior.AbstractAjaxBehavior; import org.apache.wicket.request.IRequestCycle; import org.apache.wicket.request.handler.resource.ResourceStreamRequestHandler; import org.apache.wicket.request.resource.ContentDisposition; import org.apache.wicket.util.file.Files; import org.apache.wicket.util.resource.FileResourceStream; import org.apache.wicket.util.resource.IResourceStream; // 基于Wicket官方示例实现的AJAX文件下载行为 public class AjaxFileDownload extends AbstractAjaxBehavior { private static final long serialVersionUID = -3721931990952363001L; private File file; private boolean deleteAfter; private boolean addAntiCache; public AjaxFileDownload() { this(false, true); } public AjaxFileDownload(boolean deleteAfter) { this(deleteAfter, true); } public AjaxFileDownload(boolean deleteAfter, boolean addAntiCache) { super(); this.deleteAfter = deleteAfter; this.addAntiCache = addAntiCache; } // 触发下载的方法 public void initiate(AjaxRequestTarget target) { if (file == null) { throw new IllegalArgumentException("未提供要下载的文件。"); } String url = getCallbackUrl().toString(); if (addAntiCache) { url = url + (url.contains("?") ? "&" : "?"); url = url + "antiCache=" + System.currentTimeMillis(); } // 延迟跳转以释放Wicket请求通道 target.appendJavaScript("setTimeout(\"window.location.href='" + url + "'\", 100);"); } @Override public void onRequest() { ResourceStreamRequestHandler handler = new ResourceStreamRequestHandler(getResourceStream(), getFileName()) { @Override public void respond(IRequestCycle requestCycle) { super.respond(requestCycle); if (deleteAfter) { Files.remove(file); } } }; handler.setContentDisposition(ContentDisposition.ATTACHMENT); getComponent().getRequestCycle().scheduleRequestHandlerAfterCurrent(handler); } private String getFileName() { return file.getName(); } private IResourceStream getResourceStream() { return new FileResourceStream(new org.apache.wicket.util.file.File(file)); } // 设置要下载的文件 public void setFile(File file) { this.file = file; } }
下载按钮点击逻辑
private AbstractButtonPanel createDownloadTemplateButton(String markupId) { return new ButtonPanel(markupId) { @Override protected void buttonClick(final AjaxRequestTarget target) { FormExceptionHandler<File> handler = dataHolder -> generateCsvTemplateFile(); File templateDownload = handler.apply(new WicketExceptionDataholder(target, form, log)); fileDownloadBehaviour.setFile(file); fileDownloadBehaviour.initiate(target); } }.withLinkTextKey("downloadTemplateButton.label"); }
ButtonPanel类
import org.apache.wicket.ajax.AjaxRequestTarget; import org.apache.wicket.ajax.markup.html.AjaxLink; public abstract class ButtonPanel extends AbstractButtonPanel { public ButtonPanel(String markupId) { super(markupId); } @Override protected AjaxLink<Void> createAbstractLink(String markupId) { return new AjaxLink<>(markupId) { @Override public void onClick(AjaxRequestTarget target) { buttonClick(target); } }; } protected abstract void buttonClick(AjaxRequestTarget target); }
AbstractButtonPanel类
import com.xyz.uof.frontend.web.bootstrap.css.BootstrapCssEnum; import com.xyz.uof.frontend.web.bootstrap.css.BootstrapCssEnum.BUTTON; import java.util.Optional; import org.apache.commons.lang3.StringUtils; import org.apache.wicket.Component; import org.apache.wicket.behavior.AttributeAppender; import org.apache.wicket.markup.html.WebComponent; import org.apache.wicket.markup.html.basic.Label; import org.apache.wicket.markup.html.link.AbstractLink; import org.apache.wicket.markup.html.panel.Panel; import org.apache.wicket.model.IModel; import org.apache.wicket.model.Model; public abstract class AbstractButtonPanel extends Panel { /** * 配置按钮样式(链接、主按钮等) */ private String buttonSizeClass = BUTTON.SIZE_SMALL; private String cssClass = BootstrapCssEnum.BUTTON.PRIMARY; private String linkTextKey; private String buttonName = "submitButton"; // 默认值 private IModel<Boolean> visibleModel = Model.of(true); private IModel<Boolean> enabledModel = Model.of(true); private String iconClass; private boolean spinnerActive = false; public AbstractButtonPanel(String markupId) { super(markupId); setOutputMarkupId(true); } @Override protected void onInitialize() { super.onInitialize(); add(createLink("link")); } @Override protected void onConfigure() { super.onConfigure(); setVisibilityAllowed(visibleModel.getObject()); setEnabled(enabledModel.getObject()); } private AbstractLink createLink(String markupId) { AbstractLink ajaxLink = createAbstractLink(markupId); ajaxLink.add(new AttributeAppender("class", buttonSizeClass + " " + cssClass, " ")); ajaxLink.add(createSpinner("spinner", ajaxLink)); ajaxLink.add(createLabel("label")); ajaxLink.add(createIcon("labelIcon")); ajaxLink.get("labelIcon").add(new AttributeAppender("class", iconClass, " ")); return ajaxLink; } protected abstract AbstractLink createAbstractLink(String markupId); private Component createSpinner(String markupId, AbstractLink ajaxLink) { if (spinnerActive) { ajaxLink.add(new AttributeAppender("onClick", String.format("document.getElementById('%s').childNodes[1].className = 'spinner-grow spinner-grow-sm';", ajaxLink.getMarkupId()))); } return new WebComponent(markupId) { @Override protected void onConfigure() { super.onConfigure(); getParent().getId(); setVisibilityAllowed(spinnerActive); } }; } private Label createLabel(String markupId) { // 处理空链接文本的情况,支持纯图标按钮 return new Label(markupId, () -> StringUtils.isNotBlank(linkTextKey) ? getString(linkTextKey) : "") { @Override protected void onConfigure() { super.onConfigure(); setVisibilityAllowed(StringUtils.isNotBlank(linkTextKey)); } }; } private Component createIcon(String markupId) { return new WebComponent(markupId) { @Override protected void onConfigure() { super.onConfigure(); setVisibilityAllowed(Optional.ofNullable(iconClass).isPresent()); } }; } public AbstractButtonPanel withButtonSizeClass(String buttonSizeClass) { this.buttonSizeClass = buttonSizeClass; return this; } public AbstractButtonPanel withCssClass(String cssClass) { this.cssClass = cssClass; return this; } // 供Selenium测试使用 public AbstractButtonPanel withName(String... buttonName) { StringBuilder sb = new StringBuilder(); for (String currentSplit : buttonName) { sb.append(currentSplit); } sb.append(this.buttonName); this.buttonName = sb.toString(); return this; } public AbstractButtonPanel withLinkTextKey(String linkTextKey) { this.linkTextKey = linkTextKey; return this; } public AbstractButtonPanel withVisibleModel(IModel<Boolean> visibleModel) { this.visibleModel = visibleModel; return this; } public AbstractButtonPanel withEnabledModel(IModel<Boolean> enabledModel) { this.enabledModel = enabledModel; return this; } public AbstractButtonPanel withIconName(String iconName) { this.iconClass = iconName; return this; } public AbstractButtonPanel withSpinner(boolean spinnerActive) { this.spinnerActive = spinnerActive; return this; } }
内容的提问来源于stack exchange,提问作者clatadia
相关产品推荐
相关产品推荐

